* fix(webworker): load plugin manager and HMR in the preview Worker
The Worker disabled both rows, which left Creator mode's
tool-plugin-manager waiting for its service and the Plugins page without
a manageable profile. Only package commands are unavailable; the
services load. The Worker provides appReady from its own readiness
promise, resolved once the tunnel serves, and its process shim reports
no entry script, so HMR keeps only its profile configuration watches.
Fixes#5372
* fix(webworker): show xdg-open text files in a preview viewer
The Worker implements xdg-open as a platform executable that sends one
VFS text file to the page, which shows it in a read-only modal dialog.
Settings' "Open configuration file" now opens the profile patch in the
preview instead of failing on a missing desktop opener.
Fixes#5371
* fix(dockkit): keep floating headers below the Windows caption
* fix(windows): preserve caption gaps across dialogs and fullscreen
* fix(client): unify overlay clearance and preserve the Windows caption
* docs(desktop): explain fullscreen IPC platform guard
Fixes the review findings on the work-budget change:
- unify the measured npm time range to 137-205 s in the note's problem
statement, which had carried the older 153-190 s range
- correct the peer-edge share to about three quarters (1874 of 2524),
which the sentence had called two thirds
- state the ratchet threshold as the packages-times-edges product, about
12% in packages and edges together
- derive NPM_HANG_GUARD_MS from MAX_RESOLUTION_WORK_UNITS so a raised
budget cannot shrink the guard's headroom, and state the guard's
guarantee as covering a runner four times slower than the measured host
- name the cost side of the hang guard in the note's consequences
The Dependency layout lane failed whenever the npm child process exceeded
a hardcoded 300 s deadline. That deadline measured the runner rather than
the graph: the same layout resolves in 137-205 s on an idle developer host
and exceeded 300 s on CI while the placement assertion reported no error,
so the failure carried no information about what had grown.
The gate now bounds the verified graph by resolution work units,
dshPackagesPerVersion * checkedDshEdges against MAX_RESOLUTION_WORK_UNITS,
and keeps the wall clock only as a hang guard derived from that budget.
Growth fails with the graph's own measured counts on any runner.
The resolution cost itself is unchanged. Measurement attributed 99.3% of
it to npm's peer-conflict placement, and cache reuse, graph splitting and
the placement flags were measured and rejected.
The off-state thumb reused the palette's primary foreground, which resolves
to near-black in dark mode; against the translucent track it read as
disabled, while a disabled control differs only by half opacity. Give the
off state its own thumb color through the new `--dsw-alias-switch-thumb`
alias: white in light mode and neutral-bluish 400 in dark mode.
The plugin management info button also becomes an 11px caption-colored icon
in a 20px square, keeping the 4px gap beside the subtitle.
Refs #5347
The rejection of a longer fixed wait or an unbound retry already lives in the
two linked notes; this note now keeps only what is specific to the case, that
`findByText` resolves on a commit that does not drop the panel.
The note links the two 2026-09-27 testing notes that record the same decision
family, and both link back. Line-number anchors give way to file and symbol
references, the deferred-refresh alternative the neighboring cases use is
recorded with why it was not chosen here, and the negative control states that
the close call is removed.
The diagnosis script suite asserted that repaired paths are owned by the invoking user. On an elevated Windows runner the fixture objects are owned by BUILTIN\\Administrators, so the assertion failed without any repair defect. Compare the owner before and after the repair instead, which keeps the stated owner-preservation guarantee without assuming the host token's default owner. Verified locally: 34 tests pass.
The case awaited the empty list's text and then read the panel once. The
detail closes through the effect that observes the removed row, one commit
after the render that replaces the list with its empty state, so that single
read could still see the panel. The assertion now waits for the state it
asserts, and a panel that never closes still fails.
Refs #5332
The bundled diagnosis skill told the model to run its script unconfined from the first call, which forbids trying the confined path and leaves no path but handing the command to the user. Run it in the current sandbox first, and request one escalated retry of the same call when a denial or an unwritable report directory prevents it; a refused or unavailable approval reports the path as undiagnosed. Refresh the recorded skill replay and re-record the README pairing hashes.
Packed consumers resolved ^3.1.0 to 3.3.2, whose source build fails under the Linux release check. Pin every consumer to the existing lockfile version 3.1.1 so published installs use the tested dependency.
Seven cases, not nine, carried 90_000 before the change; the other seven
ran on the default and five of those spawn. Record the lane-flag
alternative, cite the archived describe-level budget notes and the
accepted risk, and bound the idle-host timing claim to the measured
cases.
Addresses ds-review-bot: the keyboard step now asserts that focus landed on
the sheet overlay before pressing arrows, the comment keeps only the local
why, and the Agent Note carries the measured counts (10 of 11 master runs,
7 of 38 probe runs, 26 of 26 keyboard runs, 10 of 10 local repetitions).
Five cases in scripts/oxlint-contract.spec.ts spawned oxlint or the
repository lint entrypoint on vitest's 5000 ms default while their nine
siblings carried the 90_000 ceiling from the Windows spawn-budget
alignment. The first darwin parity run on master (Sandbox run
36332053320) ran the file 2.2x slower than the passing run and timed out
`prints only the final diagnostics when a fix retry still fails` at
6309 ms. Give the five cases the same per-case ceiling and record the
decision in an Agent Note.
Fixes#2581
Related to #2677
The meeting-sheet step of document-preview.e2e.ts clicked the sheet overlay
at the cell FortuneSheet already selects on mount, so its polls passed before
the click committed, and the press resolved its cell inside a React state
updater that later renders re-execute against the reflowed grid. On loaded
Linux runners that re-execution landed after the first sidebar resize and
committed B1 (the two-column sheet clamps the stale pointer x to its last
column), failing the reflow loop with `expected 'B1' to be 'A1'`.
Select A1 through ArrowRight/ArrowLeft instead: keyboard moves compute the
target from committed state, so a re-executed updater yields the same cell,
and the B1 -> A1 transition verifies the interaction before the reflows.
Refs #5242
Review follow-up: name the parity job in the workflow header, point the unit
comment at ci.yml's Linux jobs instead of a nonexistent `checks` job, state the
budget as a completion ceiling and raise it to 60 minutes so the slowest observed
runner keeps headroom, and let the owning Agent Note describe the budget and the
workflow's statuses without restating numbers the workflow owns.
The seatbelt leg of the sandbox-e2e matrix also ran `pnpm run test`, and the
complete unit suite takes 16 to well over 20 minutes on macos-latest, so the
shared 20-minute job budget cancelled the leg on most master pushes — before
the Seatbelt confinement e2e ever ran. Move the parity run into a `unit-darwin`
job with a 45-minute budget; the four e2e legs keep 20 minutes.
Review round 1: the tolerance now requires the accumulated tail to remain a
prefix of the controlled prompt, so a prompt that arrived and was invalidated by
later output keeps the plain silence bound instead of waiting a tolerance with
nothing left to wait for. The field is validated as zero or at least one
`pollIntervalMs`, matching the handoff grace rule, and tests pin the extended
bound's expiry, the invalidated tail, and that rule. The owning readiness design
note records the new field and Tier 2 bound and cross-links this decision.
Windows sessions on the self-hosted lane settled one or two of seven sends on
the controlled prompt and charged the silence tier for the rest (issue 2487).
The marker is written by the shell's own prompt function and the printable tail
by the same render, so a tail that arrives past `idleSilenceMs +
handoffGraceMs` means a stalled delivery, not an absent prompt.
`dsh-terminal-bash` gains a validated `promptTailGraceMs` bound (default 0,
which reproduces the previous bound exactly). While a send has seen the marker
and its printable tail has not arrived, the silence bound grows by it. The
pwsh Loader-composition case sets 5000 ms, the host class the bound is for, and
still fails when no marker appears at all.
Review found two factual errors in the prose this change adds.
plugin-install-cancel: the scenario pauses the page clock before the
enable click, so the highlight's `setTimeout(clearHighlight, 2400)`
cannot fire while the status poll runs. The comment claimed a reload
could outlive that expiry, and the note explained the two CI timeouts
at the untouched highlight locator the same way. The cause of those
timeouts is not diagnosed, so both now say so instead of offering a
mechanism the paused clock rules out. The pre-change file reproduces the
same hang under the same lane command, which the note records.
workspace-new-session-folding: `nth(7)` is the first row the batch
reveals, not the last established one, and the reflowing `showMore.click()`
is above the comment, not below. Both are corrected, and the workspace-row
comment names what `:hover` reveals instead of restating a cell swap the
project row does not have.
turn-tail-actions: clear the pointer modality flag with `Shift`, which
moves no focus, matching focus-rings.e2e.ts.
The zh dictionary rendered subagent, list_agents, interrupt_agent, and
ralph tool cards with 代理 (proxy) for Agent. Use 智能体 like the goal hint
and wait_agent copy already do.
The self-hosted Windows serial lane failed 12 cases from one cause: the host
exported Git's indexed command-line configuration (`GIT_CONFIG_COUNT` with
`GIT_CONFIG_KEY_0` / `GIT_CONFIG_VALUE_0`), and the shared subprocess scrub
removes credential-shaped names, so a child inherited the counter without its
key and died with `error: missing config key GIT_CONFIG_KEY_0` before it read
any configuration file.
Both spec files now delete that group from their own environment before they
spawn Git, because they judge profile behavior rather than host state. The
product-side handling stays out of this change; it is preserved on the branch
`deferred/git-command-line-config-scrub-20260927` for a later product PR.
GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.bare GIT_CONFIG_VALUE_0=true pnpm exec
vitest run packages/boot/plugin-manager/tests/github-connection.spec.ts
packages/boot/plugin-manager/tests/manager.spec.ts -> 12 failed at the parent
revision, 95/95 passed with the isolation.
The three-file lane run passes all three scenarios in three consecutive
local runs, and the install-cancel scenario keeps a residual exposure:
it finds the card through the highlight, so a reload that outlives the
highlight while the card is absent fails that locator. One lane run
failed there and it did not reproduce.
Master's 07dcdb46e5 moved the question step's settled title into the
`Ask question waiting` row of mid-steer.expected.md, so the button this
scenario waited for no longer renders. The wait therefore never resolved and
parked the scenario on the question page, producing the lane's cascade: `no
turn/end within 30000ms`, no live root session matching session.v4.jsonl, and
`llm-replay: fixture not fully consumed`. `captureStableAria` already polls
until two consecutive normalized captures agree, so the settled state is
captured without the extra wait.
DSH_SNAPSHOT=replay pnpm exec vitest run --config vitest.web.config.ts
apps/web/tests/steering.e2e.ts -> 7 passed (reproduced twice); with the wait
present the same command fails at its 10 s bound and again widened to 30 s.
Three scenarios captured an aria region while the state their golden
records was implied by the pointer, the keyboard, or an in-flight Host
read, so the capture could land on the other state.
workspace-new-session-folding captures two goldens of one sidebar, each
recording a hovered row that Rows.module.css renders with the row
actions in place of the relative time. Both hovered rows are now
established before their capture instead of inherited from wherever the
Show-more click left a stationary pointer. The serial self-hosted master
lane failed this scenario in every failing run observed between
2026-09-24 and 2026-09-27.
turn-tail-actions records a tooltip beside the focused Copy footer in
running.expected.md. Tooltip ignores focus while the last input came
from a pointer and only a keydown clears that flag, so the scenario now
establishes keyboard-owned focus and waits for the bubble.
plugin-install-cancel polls the enabled card's status instead of
sampling it once. The store marks the card and starts the directory
reload the Host answers, and the status flipped disabled -> running 61 ms
after the highlight in three local runs, which the single read sampled
inside.
Goldens, product code, and assertion strength are unchanged.
`operations.spec.ts` read the profile's run record from a 100 ms timer armed
before the refused installation whose repair it observes; a loaded runner
reached the repair run after that timer and the case failed with `null`
against the recorded run. The observing child now stays in flight until the
record appears, and the bounded wait starts at the launcher hand-over rather
than at test start.
The loader-composition case counts `stdin_read` settlements but reported only
the count, so its Windows failures could not say which tier settled which
send. The recorded reasons now ride in the assertion message; the threshold
and the assertions are unchanged.
Extract settleRefresh() so the spinner-minimum hold has one settle point,
advance the e2e fake clock past the 400ms hold before asserting the button
restores, and drive the disposal-during-hold store test on fake timers for
both success and failure reads.
The install registry picker used MenuSurface and overrode the theme-owned
menu material tokens, which the shared-menu-material gate rejects. Return
the picker to a fieldset (its native group role) and paint an opaque
bg-layer-2 surface so the form controls stay readable without touching
menu tokens. This also restores the group's radios to the accessible tree.
Align plugin-install-registry.e2e and its snapshots with the PR's visible
text: registry option names drop the parentheses around the host, the
custom address field is no longer disabled, and the custom hint spells out
http(s) and ~/.npmrc.
Hold the manual-refresh spinner to a 400ms minimum so a fast read does not
flash it, clearing to idle (or the failure toast) only after the minimum,
and dropping a refresh that settles after disposal.
Prune the retired no-unknown-casts baseline entry for browser-plugin.client.spec.
Drop the plugin-manager refresh e2e to a single failure round: the store's
success and retry outcomes are covered deterministically by
manager-store.client.spec.ts, while the e2e keeps its unique value — real
500ms tooltip timing, a running spinner animation, aria-busy, and the
portaled failure toast over kept cards. Regenerate refresh.expected.md.
Remove the components spec tooltip-delay case; ui-primitives'
tooltip.client.spec.tsx already covers 499/500ms hover/focus delay, early
cancellation, and portaling. PluginManagerPage.tsx stays at 100% coverage.
Closing the provider editor confirms its write, but the browser directory
refresh lands after it, and the reopened editor copies the previous input
selection into a new draft — checkbox polling cannot correct a draft that
was already built. Run 36159502081 (job 108152262196) read a stale `true`
from that draft at models-settings.e2e.ts:395.
Wait for the saved-provider notice, which the refresh publishes, before the
third reopen. The adjacent reopen in the same test file waits on the same
notice.
The browser lane pins the browser to Asia/Shanghai, and formatMessageClock
prefixes clock.md / clock.ymd to the clock once a message's local calendar
day differs from the renderer's. Four scenarios left both clocks on the
wall clock while their message times are stamped during the run — a live
composer send by the Host, a `createdAt: 0` seed anchored from Date.now()
— so the job that began six minutes before local midnight rendered
`9/25 23:58` where their goldens record `{{clock}}` (run 36157253018, job
108144715986: steering, markdown-images, message-actions, and
reference-composer failed together, then passed in the next run).
Read WEB_FIXTURE_TIME on both clocks instead. Live-stamped steering suites
install pinHostClock(), a Date.now spy that advances from the anchor with
real elapsed time; pinBrowserClock() re-applies one frozen instant from the
anchor on an interval, because holding a single instant removes elapsed
time from the 150 ms process-title hold and the composer's 10 ms
composition guard, which then never elapse. Seeded scenarios anchor their
seed beside the pinned browser clock, which keeps the relative-age buckets
their goldens assert.
The steering mid capture waits for the settled question title its golden
records, and the whole-queue scenario waits for the running turn before its
queue gestures, whose decision resolves against an agent that reports busy.
Goldens, the Asia/Shanghai browser timezone, and product rendering are
unchanged.
Reading Git's path text verbatim to keep trailing whitespace duplicated the
line-terminator helper that scripts/change-scope.ts already owns, which the
duplication gate rejects. Canonicalizing the fixture-side common directory
with realpathSync.native removes the text parsing entirely: short names,
junctions, and directory symlinks all collapse onto the physical directory
the installer resolves, and the injected path stays exact.
Drops the trailing-space case: with no text trimming left in the
derivation, that path already matched the installer before this change, so
the case could not fail and proved nothing.
git() trims its output, which also drops a worktree directory's own
trailing whitespace; the installer removes only the line terminator. Derive
the expected lock path through gitPath()/stripGitLineTerminator(), which
applies the installer's exact rule, so the same-string claim holds for
every path.
Adds the trailing-space injection case (skipped on Windows, which cannot
create such a directory); it fails with the same probe ENOENT under the
trimming derivation.
The five lock-access cases only diverged on a host whose temp directory
already carried a short name or directory alias. Route one case through an
aliased worktree root (a junction on Windows, a directory symlink
elsewhere) so the installer's resolved common directory necessarily differs
from the path this spec composes; the case fails with the same probe ENOENT
under the previous derivation on every platform.
The alias case shares the injection body through
expectInjectedLockAccessFailure, so the five existing case names and their
assertions are unchanged.
The lock-access cases injected a filesystem failure on a path this spec
predicted from the mkdtemp fixture directory. The installer resolves its
common directory against Git's own top-level path instead, and the host
spells that path differently: Git canonicalizes Windows 8.3 short names
and the macOS /var symlink. When the spellings diverged the injection
never fired, so the probe file was never written and all five cases
failed with ENOENT at the probe read - the signature seen in the Windows
coverage lane.
Resolving the relative common directory against Git's top-level, as the
installer does, makes the expected path identical to the one the
installer touches on every platform. The five cases fail deterministically
on macOS before this change and pass after it.
`ToolCallRecovery.observe` compared `entry?.turn` against the event's turn
before reading `entry.step`. For a `tool/result` whose callId never registered
as pending and whose event carries no turn/step, both sides were `undefined`,
the guard passed, and the `entry.step` read threw. A pending entry is now
required before the turn and step comparison.
The bundle no longer declares `dsh.bundle.rowSwitches`, so its page offers a
switch per row while it is on, as every other bundle does. The README records
that the three rows work only together, and the Agent Note records the
rejected lock.
With `dsh.bundle.rowSwitches: false`, `listPlugins` reports each row the
bundle inserts with the `bundle-switch` read-only reason, so
`setPluginEnabled`, the `plugin_manager` tool, and the Plugins page agree,
and the page draws no switch for such a row. `bundleRowSwitches` validates
the manifest field; `listBundles` reports a non-boolean value on the bundle.
`BundleInfo.rowSwitches` is removed.
A stale-file response naming many file ids made RequestFiles.retry invalidate
each mapping through its own writer-lock cycle on files-v3.json. The lock waits
two seconds with a fixed backoff cadence, so concurrent contenders advance one
per round and most of them time out once a response names more than about a
dozen ids; the timeout surfaced as TRANSPORT and the replacement request never
left. DeepSeekUploadIndex.remove and DeepSeekFileStore.invalidate now take every
rejected generation at once and rewrite the index under one lock acquisition.
The language-switch case now reads the Automation tasks row's English title
and description, and the English guide writes the UI label as plain text like
the rest of the guide.
The visible id chip and the accessible name now share one check for whether
the entry id adds to the title, so an include: id that matches the title is
left out of both. The loading skeleton's status region carries visually
hidden text so screen readers announce it. The settings-chrome browser
scenario expects the preset group open and enabled rows without a tag, and
its two ARIA snapshots are refreshed. The row-partner CSS comment states its
two-column assumption.
The bundle README said the right sidebar shows the Automation tasks page. The
ui-schedule client registers that page as a `main` panel with a sidebar entry and
keeps the right Sidebar for the single-task detail tab.
The Web composition no longer carries `time-context`, `schedule`, and
`ui-schedule`. The optional bundle inserts the three rows and depends on their
packages, as the other optional bundles insert theirs, so the Plugins page lists
them under the bundle; `dsh.bundle.rowSwitches: false` keeps the bundle's own
switch as their only control. The three packages gain localized titles and
descriptions for that list.
A bundle whose rows work only together sets `dsh.bundle.rowSwitches: false`.
`listBundles` reports it as `rowSwitches`, and the Plugins page then lists the
bundle's rows with their state and offers only the bundle's own switch. A
bundle that omits the field keeps a switch per row.
The 0.1.7-rc.2 release bumped the root and every dsh-family manifest; the new
experimental bundle carries the same version so check-workspace-constraints passes.
The 2026-09-16 host-schedule-storage note now names the optional experimental
bundle as what mounts Schedule, since the Web composition carries the row disabled.
The opt-in note's Decision drops the sentence the Problem already owns, and the
assembled-remote mock states why its Schedule answers stay declared.
The overlay alternative now says only its insert form fails to override a shipped
row, since an id-targeted --patch overlay does set one. The composed assertion
compares the overridden row's package name with the shipped layer's name instead of
asserting a string.
Cards in one grid row now share a height, with title rows and descriptions
starting at the same offsets. The entry id shows only when it differs from
the title, as a code chip in the card's bottom-left corner; tags mark only
states other than plainly enabled; descriptions clamp to two lines until the
card expands; the preset group starts open; loading shows skeleton cards.
Group headers share one height, the subtitle separator is evenly spaced, the
title weight drops to 500, 12.5px text moves to 12px, and a container query
on the tab switches the grid to one column.
Cross-link the archived decision the opt-in note supersedes, state the Loader's one
entry per id and last-declaration-wins behavior in the overlay and extraction
alternatives, scope the missing-row warning to profiles that mount no Plugins page,
and move the switch-on-but-ineffective case to the list-order paragraph. Assert one
top-level composed row per id and correct the stale assembled-remote comment.
Archive the reversed 2026-09-24-web-default-schedule-composition triplet and name
the four bundles in the optional-bundles note. Rewrite the opt-in note's Problem to
the state at the branch base and state why the bundle switches time-context on with
schedule. Record the missing-row warnings, the bundle list order, and the bundle
detail page's empty state in the bundle README. Pin the composed Schedule rows in
scripts/optional-bundles.spec.ts and move the cli dependency entry into order.
The row now carries the name of the sidebar entry it adds and a
plain-language description, and its icon is an alarm clock drawn in the
Official group's 36px style. The README and the user guide quote the new
title; the README names the sidebar entry instead of a right-sidebar page.
A denial this backend cannot explain — the confined child cannot open an object it
was granted, or provisioning itself fails with Win32 5 — reached the user as a bare
"Access is denied". The README documented both causes but shipped no way to tell
them apart.
Add the `diagnose-windows-sandbox-acl` skill. Its script reads unless a repair switch
is passed, and classifies the failing path and its ancestors before anything
changes; its two repair modes are the only supported ACL edits. `-Fix` removes a
foreign package-SID allow ACE, and `-GrantFullControl` supplies the
WRITE_DAC/WRITE_OWNER pair the mandatory-label write needs without ever changing an
owner. Both require `-AllowRoot`, refuse a target outside it, and refuse to run
together.
`dsh-sandbox-local` registers the skill on Windows through `ctx.inject(['skills'],
...)`, so the skill registry stays optional and the sandbox never depends on it. The
assets ship in this package's `files` list, which the workspace-constraints gate now
pins alongside the runner and emitted-type entries.
Tests cover the script's verdicts (foreign ACE, healthy object, inert capability
SID, inert DENY ACE), its surgical removal, the grant's owner invariant, and both
refusals; the catalog spec covers registration and disposal.
The shipped Web composition carries `time-context`, `schedule`, and
`ui-schedule` disabled. This adds
`@deepseek-ai/dsh-experimental-schedule-bundle`, whose patch sets
`disabled: false` on those three rows and inserts none, and names it in
`OPTIONAL_BUNDLES` with a runtime dependency of `apps/cli`, so every
installation ships it switched off and the Web Plugins page offers it in
the Official group.
The two Web suites and the built-Web Schedule Remote scenario enable the
rows through that bundle instead of a scenario-local fixture.
Review found three ways the takeover could let two writers overlap.
- A claimant that read an exited PID could remove the lock of a new
holder that reused that PID before the claim. The claimant now probes
the PID again under the claim, after re-reading the record.
- The browser Worker's process shim reports its own PID as absent, so a
second writer in the Worker took over the first one's live lock. A
record naming the contender's own process is never taken over.
- Takeover proves only that the holder exited, while a pnpm run it
started can still be writing the profile. Each profile operation now
records its pnpm run in .plugin-manager/run.json and removes the record
when the run ends; an operation that finds a record waits for the run
within the existing tree bound and otherwise refuses, naming the
process and the record, without running pnpm.
A failed removal of the exited holder's lock now waits instead of
failing the acquisition, the probe range is stated as int32, and the
tests inject exited PIDs instead of reusing real ones, which Windows
reissues at once.
The tools pipeline's ask decision now carries an optional displayReason
that ToolRuntime forwards to the approval service, so the Auto review
prompt is localized while the audited reason stays English.
Adds the auto-review-approval Web snapshot for a user rejection after a
reviewer denial and for a reviewer failure, and updates the permission
preset pages, READMEs, Agent Notes, and child-agent JSDoc that still
described the shared never bundle.
The takeover note said no Windows binding exists for a kernel-released lock; the Session write lease already holds a named semaphore there through koffi.
skippedProfileBundles recomputed the selected-minus-loaded bundle set from
the manifest, a second home for what loadProfileDirectory already records.
Runtime resolution and generateConfigSchema now read
profile.skippedBundles, so generateConfigSchema no longer reads the profile
manifest and drops its binName parameter.
Optional request extension fields had no size bound. A very large field
could exceed the runtime's string limit while the request was serialized,
which failed every attempt, including retries.
- Bound the extension payload carried by one request (8 MiB by default,
configurable) and send the remainder with later requests.
- If the combined request still cannot be serialized, send the base
request without extension fields and log which fields were omitted.
- Add regression tests and update the related references and decision note.
The seat kept the pending row in local state, so a stale settlement after
connection/reset could clear a newer selection's spinner, a remount lost
it, and /model popup selections never marked a row. ModelDirectory now
publishes the submitted selection as pending under its generation guard.
The trigger spins whenever a selection is pending, so leaving the row's
pane keeps the feedback visible.
The hostname guarded only writers on several hosts sharing one file,
which no supported deployment does, and macOS changes its hostname when
networks change, so locks from before the change could not be taken
over. The nonce closed a race that needs the PID space to wrap between
two reads. Keeping the `<pid>\n` record lets processes of both releases
interoperate.
A process that ends without running withFileLock's finally left
<file>.lock behind, and every later writer timed out until an operator
deleted it. `dsh plugin` interrupted with Ctrl-C, `dsh web` exiting
right after disposal while a cancelled install still restores files,
and any crash all left the profile package lock this way.
The lock now records its holder's PID, hostname, and a nonce. A
contender that finds a record from this host whose PID a signal probe
reports absent claims that record through a wx-created claim file,
re-reads the lock, removes it only while it still holds the record, and
retries at once. A PID-only record from an earlier release counts as
written on this host, so locks left before this change are taken over
too. Empty, incomplete, foreign-host, and live-holder records are still
waited for.
loadProfileDirectory printed each skipped bundle on every call, and startup,
runtime resolution, the config editor, the plugin manager, and HMR all load
the profile, so one incompatible bundle printed the same warning six times at
startup and again on each Web page load. Loading now returns the skips in
Profile.skippedBundles, and the CLI and Desktop launchers print them once per
start with reportSkippedBundles.
session.selectModel can take seconds while the Host persists the default
model. The composer seat disabled its rows with no other feedback; the
chosen row now spins in its check slot, and the trigger spins in place of
its chevron while the menu is closed.
- State the contract precisely: the cut never introduces a lone high
surrogate, and an unpaired surrogate the input already carries is not
repaired.
- Document the new export in the package README: a character-budget preview
section naming the persistent bash and pwsh shells and str_replace_editor,
plus a Known Limitations bullet that no longer contradicts it.
- Drop the assertion restatement from the two shell test comments.
- Let the str-replace test helper reuse a root so the clipped context reads
its file inside its own working directory.
The shipped Web bundle gives every live root Agent the Host Schedule
tools, and every other scenario in this file that boots the shipped
base/web bundle with the minimal preset expects ['bash',
...SCHEDULE_TOOLS]. This case uses the same bootWeb composition and adds
only a settings patch, which does not change tool composition, so its
['bash'] expectation failed on every PR.
A reviewer denial now returns the tools pipeline's ask decision when the
Session's approval policy is ask, so the user can approve, reject, or
cancel the call. Selecting Auto writes the ask policy; a recorded Auto
selection still matches never, which delegated children pin, so their
denials stay final.
A reviewer failure now fails the call with its specific error instead of
presenting it as an Auto review denial.
Keep the timing rules that apply to every selector in the create
description once, move the cron day-field and fixed-rate alignment rules
onto their parameters, and drop parameter formats, result fields, and
Host compare-and-set details from the tool descriptions.
Drop the extension-less path hint from read_image and the
returns-before-stopped note from job_kill, and describe send_message
delivery without "resumes a turn". image-compaction lowers its context
window by the 30-token tool-schema estimate reduction.
Tool guidance sections keep only what the tool definitions do not say.
Apply review: reword interrupt_agent and list_agents, carry "a rejected
escalation stays denied" in the rejection error, pin
sandboxPermissionsDescription, and fix stale README and Agent Note
statements. Record the tool-definition rules in agent-experience.
Compaction threshold snapshots shift their context windows by the
370-token system-prompt estimate reduction; escalation-rejected moves to
a v4 fixture for the new rejection text.
Delete constraints the model learns from the call result, shorten
implementation detail, and move per-parameter rules into the owning
parameter descriptions. Bash, pwsh, and fs share one
sandbox_permissions description in dsh-sandbox.
The standard preset's first-turn prompt drops from 7985 to 6551 tokens.
Compaction threshold snapshots lower their context windows by the
1548-token tool-schema estimate reduction so each trigger keeps its
position; their fixtures move to v4 generations.
Broaden the skill description to cover enabling a shipped plugin that a
bundle leaves disabled, add the workspace-bundle recipe for it, forbid
hand-editing profile files that install_bundle owns, and verify installs
through the install result and cordis_inspect_query instead of logs,
process lists, or paged list_plugins calls.
* feat(approval): localize prompts and allow consent for one turn
* test(approval): update cast baseline and Web PTC prompt pin
* fix(approval): cover PTC language and clarify turn consent
* fix(approval): group turn consent under allow menu
* fix(approval): keep turn consent in memory only
* test(snapshot): refresh owners for merged justification guidance
* fix(approval): drop duplicate allow-once from the allow menu
* refactor(approval): derive turn consent expiry from one check
Replace the turn approval bookkeeping (revocation state, incremental log
cursor, and turn-signal listeners) with one side-effect-free expiry check
over the log and lifetime flags. Events now only withdraw open
presentations early; the expiry check decides whether an answer or turn
consent still applies, including for Sessions without event publication.
* refactor(approval): drop one-turn consent and keep localization only
Remove the allowed-turn answer, the live turn-consent state, the allow
menu, and their tests, notes, and recorded Web scenario. The approval
service returns to the master request path; the request keeps the
localized displayReason for presentation, and the card resolves it in
the active UI language.
* test(snapshots): record the localized justification schema text in new scenarios
---------
Co-authored-by: Turtle <[email protected]>
Co-authored-by: Kaige-Gao <[email protected]>
#5108 added this case against a master where the shipped Web bundle had no
Schedule rows; #5123 then made Schedule part of the default Web composition.
Every other minimal-preset session in this file already expects bash plus
the four Schedule tools, so this one does too.
The shipped Web composition gained the four Host Schedule tools, and the
other four minimal-agent assertions in this file already expect them. The
legacy profile-patch scenario still expected bash alone, so master's E2E
lane failed on it. Align it with its siblings.
Conflict resolution:
- docs/config-catalog and docs/module-graph: regenerate with the current
generators, which write the bilingual pages together.
- The conflict came from master's new short-fingerprint pairing records: our
branch still carried the previous full-hash records. Take master's record
format, then re-record the output-retention README pair after regenerating
its two sides.
Broaden the skill description to cover enabling a shipped plugin that a
bundle leaves disabled, add the workspace-bundle recipe for it, forbid
hand-editing profile files that install_bundle owns, and verify installs
through the install result and cordis_inspect_query instead of logs,
process lists, or paged list_plugins calls.
Each record rendered its whole saved prompt, so a long instruction on a
recurring task filled the records tab with the same text once per run.
A prompt longer than two lines now shows two lines and an Expand toggle;
the toggle appears only while the clamp hides text, and a collapsed prompt
re-measures when the panel width changes.
Also drop the stale comment about the removed message-id anchor.
The shipped Web composition now puts four Schedule tools on every live root
Agent, which pushes a full-preset `cordis_inspect_query` answer past the
12,500-token inline budget, so the spill policy retains its head and tail with
a spill path instead of the complete JSON.
Refreshed owners publish a v4 generation, so every scenario that names
another owner's Session fixture must name that owner's highest generation.
The shipped Web composition also puts the Host Schedule tools on every live
root Agent, so the minimal-preset tool-table assertions include them.
* feat(experimental): ship every experimental capability as an optional bundle
dsh-base mounts the registration-only browser-use and computer-use
services; the Playwright MCP, Chrome DevTools MCP, native and MCP Cua
Driver providers, Auto review, and the Inspector become one-row bundles
listed in OPTIONAL_BUNDLES with an icon and locale display metadata.
verify-default-product-isolation requires both for every optional
bundle. Python PTC and Stagehand stay explicit compositions.
* feat(experimental): keep optional bundles inside the install closure
Drop the Playwright MCP, Chrome DevTools MCP, and native Cua Driver
bundles: each is a runtime dependency of every install and would add
about 85 MB of provider runtimes whether or not enabled. dsh-browser-use
leaves dsh-base with them. The three packages keep their locale display
metadata for component rows.
* feat(experimental): drop the Cua Driver MCP bundle and leave dsh-base untouched
* test(web): refresh Official-group goldens for the four optional bundles
* fix(plugins): use an uninstalled bundle as the install example and pin optional-bundle metadata
The install guide's example is now @deepseek-ai/dsh-subagent-codex,
a published bundle no shipped profile carries, since Auto review is an
installed optional bundle. The optional-bundle spec guards its corpus and
pins each bundle's title, description, and icon in both languages; the
Inspector's Loader test mounts its real bundle patch, its Client names
the reload a hot-enabled page needs, and its description states the
loopback DevTools endpoint's reach.
* test(web): refresh GitHub install recovery placeholder snapshot
The Web bundle now mounts `time-context` and `schedule` and leaves the
`ui-schedule` client row enabled, so the Automation tasks page, the Session
reminder catalog, and the `schedule_*` tools work without an overlay. Both
packages join the bundle's declared dependencies, and the example overlay is
deleted: re-inserting its rows would mount both Host plugins twice.
The fixed-rate floor drops from 300 to 60 seconds across the Host decoder,
the create and update validation, the model-visible tool schema, and the
client's per-unit floors.
Web recorded sessions and header sidecars are refreshed for the readings
`time-context` now appends; `normalizeWebSessionVolatiles` tokenizes their
sampled instant, browser zone, and elapsed duration so replay stays stable.
The sidebar toggle is flush with `.logoRow`, which clips its own overflow, so
the standard outward ring lost every pixel that left the row: the reviewer saw
the ring cut on the toggle in both the expanded header and the settled rail.
The 36px rail toggle is also 1px wider than its 35px row, so the control's own
right edge was clipped there. Draw this control's ring inside its own box and
stop clipping the row once the rail has settled; the ring keeps the standard
`--dsw-focus-ring-width` colour and width, and pointer modality still resolves
`--dsw-focus-ring-color` to transparent.
`focus-rings.e2e.ts` asserts the whole ring against every clipping ancestor in
both layouts, in both themes.
Only the ui-theme README pairing record conflicted; re-recorded. The stylesheet
corpus spec passes over master's new client sheets, so no ring needed realigning.
`.selected` still named `--dsw-alias-brand-primary-new-colorprimary-new-color`, the
corrupted token left by a botched rename; its dark value is `deepseek-450` while
every focus ring now uses `deepseek-400`. The selection ring reads the clean
brand-blue alias instead, so one blue covers selection and focus.
- Only the definitive Windows session-end message bypasses the quit
confirmation; the macOS power-off flag clears on the next focus or show
because another application can cancel that shutdown.
- A bypassing quit disposes the pending confirmation so a late inspection
result opens no box and an answered box cannot approve.
- Cancelling a quit that closing the welcome window started shows the
welcome window again.
- The first-hide notice records its marker only after delivery and
withdraws it on failure; the Windows quit dialog uses the multi-size ICO.
- windowShown settles when the awaited window is destroyed.
- Cross-link the partially superseded window-menus note; tidy docs.
Closing the main window hides it on macOS and Windows; the page and the
Host keep running and the next show resumes the same document. Windows
gets a permanent tray icon (open / quit) and a one-time notification the
first time the window is hidden after installation.
Every ordinary quit entry asks the Host over a new quit-inspection IPC
request for active tasks (same rule as the update restart check) and
armed scheduled reminders (schedule family of workspace/session-activity
for loaded sessions). Nothing to interrupt quits silently; otherwise a
native message box explains what stops, with Quit as the default and
Cancel on Esc. Inspection failure or a missed two-second deadline counts
as running tasks. The installer restart, fatal recovery, the development
restart command, and OS session end skip the confirmation.
Also: defer the downloaded-update confirmation while the window is hidden,
render the tray ICO from the vector icon, and point the installer's
running-application message at the tray.
Closes#4013
Resolve local image paths through the authenticated dsh-app file route and allow that route in rewritten Markdown images. Cover desktop rendering, previews, encoded filenames, and unsupported origins.
Fixes#5105.
* feat(desktop): distinguish account quota alerts from API key notices
* docs: condense architecture introduction within existing budget
(cherry picked from commit 01aacaee4d48fd7f4d00acb5e23758084d3f224e)
* test(desktop): cover quota notice lifecycle and remove unknown casts
(cherry picked from commit 7fd09407c482d1f0e30f24ba0ebf23cd844ac9a1)
* chore(account-quota): align tests, catalogs, and bilingual pairs with the account plugin
* fix(ui-settings-account): pass the Platform retry label to the account top-up overlay
* fix(chat): preserve quota failures and global notices
* fix(account): keep active top-up pages across quota notices
* fix(account): share platform page ownership and release quota holds
* fix(account): defer notices behind the shared Platform page
The single native Platform view covers the document, so a quota failure that arrives while a Settings-opened page shows is deferred rather than retained: the newest failure still replaces an older notice and its Modal appears after the viewer returns if still current. Top up remains the retained case, releasing its hold and page request with the entry.
* test(account): align quota fixture with bonus actions
* fix(account): preserve quota notice and Platform return focus
* fix(account): preserve onboarding return reads and failed notices
* docs(account): clarify page opener return ownership
* test(account): drive bonus acknowledgement retry deterministically
---------
Co-authored-by: liyao <[email protected]>
Co-authored-by: lsdsjy <[email protected]>
* fix(plugins): name the default install source by the registry it names
The add-plugin dialog showed "默认安装源" for pnpm's own registry, which
told the reader nothing about where the install would download from. Name
npm's own registry as the official npm registry, and any other registry
pnpm's own configuration names by that registry's name or host, so the
folded control reads by the registry the install actually asks.
* test(plugins): cover comparing an unparseable registry address
---------
Co-authored-by: Turtle <[email protected]>
The archived filter reaches the row toast through the registration's declared
store instead of a registrant-owned view hook, per the cross-entry view-state
rule. The README names the toast action by its actual label, and the
unarchive e2e switches back to Hide archived so it again proves the restored
row is an ordinary row in the default view.
The view-options filter is now hide/all/only radio items, the
archived-only view drops Workspaces without an archived Session, and
tree children of a dropped Workspace nest under the nearest shown
ancestor (#4854).
Only the bilingual pairing records conflicted. Master moved Agent Note records to
per-heading hashes, so this branch's note record was re-recorded in that format;
the ui-primitives and ui-theme README records were re-recorded unchanged. The
stylesheet corpus spec still passes over master's new client sheets, so no ring
needed realigning this time.
The probe needs a value that cannot be inferred from the prompt, not protected information. Describing that generated UUID as a private secret made the API test depend on an unrelated disclosure decision. Use an explicitly synthetic sample label and a benign formatting update while preserving execution audits, first-continuation cache assertions, and disabled retries.
Open the running Think disclosure explicitly when a process group also contains tool updates, and keep the fork process summary expandable in its ARIA expectation. Record the Goal request-series marker in a V4 fixture successor while preserving the historical V3 recording. Production behavior and the scroll assertions stay unchanged.
Record explicit request-series starts and the tool additions admitted when resuming a seeded parent with no tool declarations. Keep tool names and the shifted source-event reference alongside that activation history. Refresh only the affected owner-local outputs to the current writer format; retain historical replay fixtures and the unchanged child expectation.
Tool changes previously forced prompt consolidation even when the prepared
route could activate new tools through history. Rewriting the first system
message, or clearing an earlier appended prompt, reset the declaration
series and turned additions into immediate declarations.
Only routes without toolUpdate need that tool-triggered consolidation.
Capable routes append the full changed prompt and keep existing messages.
Explicit series starts, surface replacements, and unsupported prompt
routes still consolidate; retained-name definition changes still rebuild
tool declarations in Session.
Cover both update modes and reset paths, and pin the combined prompt/tool
transition through the shipped headless profile with shared schema goldens.
Run a real DeepSeek conversation through the built SDK profile and change the scoped tool registry through a model-invoked controller. Compare the first post-addition cache usage with the preceding request after unique user history has been cached; verify deferred declarations, chronological activation, and actual tool execution. Removal remains covered without requiring its cache prefix to survive. Disable test and provider retries so a warmed transition cannot hide a cache regression.
Log tool changes as developer messages independently of model capability,
fold committed headers and updates into Session.toolHistory(), and let the
LLM runtime construct deferred and retained declarations for routes that
declare a toolUpdate mode. DeepSeek serializes updates as system-role
tool_addition/tool_removal blocks with defer_loading and the tool-changes
beta header. An unchanged restored tool continues the series and is
re-offered through its recorded blocks instead of rewriting declarations.
Replace the obsolete rejection tests with wire assertions and add coverage
for the session fold, loop emission, route projection, config validation,
and the beta header. Chat and Trajectory changes land separately.
* feat(i18n): key translation pairing records by heading section
Replace the whole-file blob hashes in *.i18n.yaml with one entry per
heading section that still holds language-specific content, keyed by the
English heading-slug path. Blocks identical across the pair (generated
table rows, code blocks, shared regions) stay out of the hash, so edits to
different sections and identical regenerations merge with Git's default
text merge, including on GitHub.
Remove the dsh-translation-pairing merge driver, its resolver command and
installer wiring; gen-translation-brief recovers last-confirmed text from
commit history. Migrate all active records, archive the superseded Agent
Note, and record the replayed conflict reduction in the new one.
* test(i18n): refresh translation prompt snapshot for the terminology note
* feat(i18n): exclude only code blocks and generated regions from pairing records
Replace byte-equality masking with an explicit rule: a record hash skips
fenced code blocks and BEGIN/END GENERATED regions, the two kinds of
content the pairing gate already requires to be identical on both sides.
Move generated data into regions written into both pages:
gen-config-catalog renders both languages and the record with per-package
regions labelled by code identifiers; gen-module-graph keeps its table in a
region; gen-doc-graphs splices the event matrix regions into the Chinese
page. Add a generic region splice beside the region grammar.
master released 0.1.7-rc.1 while the package added by this branch still declared 0.1.7-alpha.2, which failed Dependency layout (verify-npm-install-layout), Pack npm tarballs (release members must share one version) and node 24 / static (constraints) from one cause. Merge origin/master and bump the new package to the root version.
The page carried a 28px top inset above its head row, so the window's topmost 28px on that page belonged to no drag region — the frame's former band covered it, and the per-row mark did not. The inset moves into the head rows themselves (.pageHead and .detailTop), which now start at the window's top edge while the content keeps its position. The browser lane pins each row's top edge at zero and the negative control confirms both assertions fail on the pre-fix sheet.
* fix(plugins): deny incompatible bundles and clarify compatibility refusals
A bundle package is not a plugin row, so startup admission never read its
own DSH peers: a bundle installed outside DSH or before a DSH upgrade still
applied its patch layer. loadProfileDirectory now skips such a bundle like an
unreadable one unless the profile exempts that exact pair.
A post-install refusal in a profile without a lockfile reinstalls from the
restored manifest instead of failing a frozen install and leaving the
rejected packages in node_modules.
Compatibility warnings now name the `dsh plugin allow-version` command for
CLI-managed profiles, startup denials name the row id instead of a resolved
module URL, and the CLI no longer reports "pnpm failed" when pnpm never ran.
* feat(plugins): report incompatible versions as typed refusals
Compatibility checks now yield the refused packages as data instead of a
profile-specific sentence. runProfilePnpm returns them as
PackageResult.incompatible, and the plugin manager refuses with the
incompatible-version code carrying the same records. Each surface renders
them: the CLI prints the exact allow-version command for its profile, the
Web plugin manager words them through its locale dictionary, and app-boot
keeps one neutral English diagnostic for logs and startup stderr.
* fix(plugin-manager): drop the agent exemption hint from the incompatibility reason
Review caught two real gaps in the previous commit.
The rename dialog's no-op check compared the localized display title, so
on an automatically titled Workspace the seeded prefill equalled it and
Rename stayed disabled — the user had no direct way to pin the visible
name. The dialog now seeds the draft with the label on screen while the
stored title decides whether confirming saves, and the duplicate check
excludes the target by Workspace identity instead of by title, since the
seeded label equals the row's own displayed title.
The registry took the automatic title from the canonical path, so a
symlink at the first-use path titled the Workspace after its target and
the default lost its localized label. The title now comes from the
requested directory's final segment; the path stays canonical.
Also: drop a rotting file count and the stale localized-path wording from
the Agent Notes, record the two cwd-derived Session label fallbacks as
known consequences, trim the WorkspaceBrowser memo comment to the
non-obvious part, and remove the deleted request type from the
hand-maintained catalog LINK_MAP.
* feat(web): restore the process-row polish on the grouped work details
The sweep highlight, shared row colors, expanded-row spacing, and the
12–24px font-size range were developed against the pre-revert work-details
implementation. Re-apply them to the grouped implementation that replaced it.
- TextShimmer keeps its active-driven span and now paints a masked sweep
overlay in the theme's `--dsw-alias-label-shimmer` color, so the base text
inherits the caller's color and hover changes.
- DisclosureRow owns the row color: tertiary at rest, secondary on hover,
with glyphs, titles, and summaries inheriting it; tool, skill, command,
reasoning, and workflow rows drop their own hover darkening.
- ChatView spaces process rows 8px apart and keeps 16px beside Assistant
responses; the grouped header sits 8px above its first member.
- The Settings font-size stepper accepts 12–24px, and the grouped header's
leading box and glyph scale with the content font-size delta.
* fix(web): restore blue running status and refine process disclosures
* fix(web): finalize running snapshots and whale animation
* fix(web): preserve disclosure layout and repair CI fixtures
Resolving packaged-app icons grew the embedded C# until the generated
-EncodedCommand line reached 32,883 characters for a 222-character path,
past the 32767-character CreateProcess limit, so association queries stopped
starting for long multibyte paths. Write the adapter to a private temporary
script and pass it with -File instead; the command line no longer grows with
the path. The temporary directory is removed once the call settles.
The icon fallback also reads the handler name the caller already fetched
instead of calling GetName again, and the README records that icon
visibility depends on resolving the Shell's indirect reference.
With no OS-marked default the split button labelled the first registered
application and marked it "(default)" in the menu, but the primary action
still asked the OS to resolve the association again; on Windows that can
prompt or open a different application than the button promises. Send that
application's id explicitly and keep the association path for the case the
Host actually marked a default.
The snapshot scenario now clears the platform's own default marker and
re-enters the file, so the association is read again instead of reusing the
answer already held, and it asserts the launched target rather than only the
labels.
Review threads: shrink READ_LANG_BY_EXTENSION to the suffixes whose own name is the better label (tsx/jsx/tf/tfvars/gradle) since the other 41 values already equal the language short name; label .hcl as hcl and .tfvars as tfvars instead of tf; accept .groovy alongside .gradle with its own short name; document that LANG_ALIASES also carries the persisted short ids; route the three inline alias lookups through grammarForHint; state that a short name may equal the grammar id; fix the util README wording.
Tests: cover .groovy, the four self-naming suffixes, and every id each suffix reaches through the shipped surfaces (the hand-synced LAZY_ALIASES list could not catch an alias whose target is neither boot nor lazy).
Compatibility checks now yield the refused packages as data instead of a
profile-specific sentence. runProfilePnpm returns them as
PackageResult.incompatible, and the plugin manager refuses with the
incompatible-version code carrying the same records. Each surface renders
them: the CLI prints the exact allow-version command for its profile, the
Web plugin manager words them through its locale dictionary, and app-boot
keeps one neutral English diagnostic for logs and startup stderr.
The retained-tab-layout note still stated that existing window-drag region pulses remain; this layer's pulse is gone and the shell watcher replaces it. Rewrite both sides and re-record the pair.
The recall watcher now tolerates a short quiet window so a report that arrives before a CSS transition moves anything cannot end the loop on its first unchanged sample, re-arms on a transition or animation that touches a marked row, and re-arms on a child list change in a container holding one. A dock picture withdraws its strip's drag claim while a pane floats, because a float sorted before that row would otherwise sit earlier in the document and lose its own subtraction. The settings panel closes when an onboarding step appears, since the step's overlay marks only #root inert. Prose and the two Agent Notes record the Windows narrowing of the Platform overlay return bar, the collection rule, and the rows' one home in the ui-theme manifest.
Node's module-hooks thread serializes resolver errors and returns the stack
accessor as a read-only configurable data property. Rewriting the routed
importer then assigned error.stack, threw a TypeError, and replaced the
original ERR_PACKAGE_PATH_NOT_EXPORTED or ERR_MODULE_NOT_FOUND error, so
plugin metadata reported a failure instead of falling back to package fields.
The importer rewrite now redefines a rejected stack value in place and
rethrows the original error with its code.
Master's newest commits add `.close` in ImageLightbox and `.imageButton` in
MarkdownText, each declaring the brand token directly. Both now read
`--dsw-focus-ring-color` through `--dsw-focus-ring-width`, as the stylesheet
corpus spec requires of every client focus rule.
A bundle package is not a plugin row, so startup admission never read its
own DSH peers: a bundle installed outside DSH or before a DSH upgrade still
applied its patch layer. loadProfileDirectory now skips such a bundle like an
unreadable one unless the profile exempts that exact pair.
A post-install refusal in a profile without a lockfile reinstalls from the
restored manifest instead of failing a frozen install and leaving the
rejected packages in node_modules.
Compatibility warnings now name the `dsh plugin allow-version` command for
CLI-managed profiles, startup denials name the row id instead of a resolved
module URL, and the CLI no longer reports "pnpm failed" when pnpm never ran.
The new no-default scenario cleared the macOS application catalog, which
does nothing on Linux: `gio mime` owns the OS default there, and the
Linux stub never wrote the launch log, so CI timed out waiting for the
application gesture. Clear the marker each platform actually reads, log
launches from both stubs, and compare the launched id against the
platform's own registration id.
Conflict resolution:
- docs/config-catalog: regenerate the English page; the Chinese page follows the
three shifted Config sources and the bilingual pair record is re-recorded.
- docs/module-graph: the generator rewrites all three artifacts together.
- packages/shell/tool-pwsh-persistent/src/index.ts: keep master's rewrite of the
prompt handling and re-apply the surrogate cap and its import.
- The three package.json dependency entries follow master's workspace:* range
convention, and the lockfile importer specifiers match them.
Master's newest agent-preset sheet carries both the new `.helpButton` and the
retained `.iconButton`, whose focus ring still declared the brand token
directly. The stylesheet corpus spec rejects that form, so it now reads
`--dsw-focus-ring-color` with the brand-token fallback.
ui-theme/README.md, README.zh.md and README.i18n.yaml conflicted again: master
extended the `design-platform.css` sentence with the `--dsw-alias-file-diff-*`
palette. Kept that sentence together with this branch's `focus.css` paragraphs
and the seven-sheet entry list, then re-recorded the pairing hashes.
* feat(preset): add creator-mode plugin practices skill
Creator mode mounts packages/preset/agent-preset/skills. The new
cordis-plugin-practices skill states Harness-specific principles and
concrete rules for plugins that survive upgrades and avoid polling or
rescanning session logs: waterfall delegation, restrict/guard over
assemble, agent.ctx ownership, no new session event types from profile
plugins, session projections, and slot-based UI.
* refactor(preset): fold plugin practices into cordis-plugin-development
A separate skill competed with cordis-plugin-development for the same
trigger. The practices now live in references/practices.md, read from
procedure step 2 before choosing an extension point.
* feat(preset): require Harness components and tokens for plugin pages
A creator-built management page served as an iframe with its own
stylesheet could not match the Harness design. The practices reference
now requires slot-rendered React pages built from ui-primitives, theme
tokens from Theme inspection, and light/dark checks in the real slot.
* feat(preset): make host UI consistency a principle and a verification step
Adds principle 6: the rendering surface decides whether a plugin can
match the Harness UI, so it is chosen before any view. Step 4 of
cordis-plugin-development now verifies primitives, token-only styles,
and light/dark rendering beside a comparable host page.
* feat(preset): prefer theme tokens over pre-stable ui-primitives
ui-primitives is a pre-stable platform module; creator plugins are plain
JS without type checks, and a throwing component blanks the slot entry.
The practices reference now treats tokens as the default way to match
the host, limits primitives to behavior that is costly to reproduce, and
tells the agent how to detect a slot entry crash after an upgrade.
* feat(preset): forbid runtime imports of Harness Client packages in plugins
Creator plugins must assume ui-primitives and other Client packages break
without notice. The practices reference now has plugins copy markup, CSS,
and behavior from primitives, or inspect the rendered host control, and
keep --dsw-alias-* tokens as the only shared styling dependency.
* fix(preset): correct plugin practices against runtime behavior
- agent.ctx registrations need a plugin-owned disposer too; plugin
unload does not remove them.
- Timers start work with followup(); inject() does not wake the agent.
- inject() logs agent/inbox/spliced at call time; agent/request cannot
change messages.
- Guards are synchronous; awaited decisions return ask from
tools/pre-execute.
- Chat node renderers are keyed by kind; slot inject reinstalls on
declaration return.
skills.spec now keeps whole-file cordis-plugin-development references
below the pruner threshold as the read tool renders them.
The reported defect is the branch where the Shell lists associated
applications but marks none as the default: the split button promoted
"Show file location" to the primary action and hid the applications
behind the menu.
Record that state in the keyless document-preview scenario. The new
golden pins the primary action to the opening control, and the gesture
log asserts the main button opens the file while the menu's marked
default launches the first registered application.
Two focus treatments still named a colour of their own: the TurnNavigator
mark's tick filled with `state-business-primary` under a comment that already
claimed the brand colour, and the plugin inventory search input drew its focus
border in `state-business-primary` while the shadow beside it read
`--dsw-focus-ring-color`. Both now read the same ring colour as the rest.
`Tooltip` states why it takes `pointerModality()`'s wide answer while focus
rings read the narrower `data-input-modality` attribute the module publishes.
The ui-web source map gains the two window-drag modules and its boot row names the watcher install; ui-settings-general states that the settings panel portals beside #root. Both pairs are re-recorded. The ui-settings-general manifest keeps master's dependency order so only the two new dev entries appear in its diff.
The first-use directory name and its stored title came from the Client's
startup language, so one product decision carried two incompatible
requirements: a path addressed by shell commands, tool arguments, @path
references, and session logs, and a label a reader reads.
The controller now owns the fixed `default-workspace` directory name and
the registry titles the Workspace after that same segment, so
`workspace.initializeDefault` takes no request at all —
WorkspaceInitializeDefaultRequest and its name validation are gone, and
the registry resolver returns a path instead of a path and title. No
language reaches the Host.
Browser consumers label a Workspace through the new
`workspaceDisplayTitle`: a title still equal to the automatic one reads
as the localized default name from the `common` namespace, every other
title reads verbatim. The Workspace browser resolves it once at the store
read, so rows, hover card, search meta, and the rename and delete dialogs
inherit it; the hero picker and the Conversation chip resolve it too.
Confirming a rename stores the text on screen, which is what stops a row
following the language.
`welcome-startup.spec.ts` imports the unpackaged main entry, which now
requires `DSH_DESKTOP_PRIMARY_RUNTIME_DIR`. Stub it beside the neighbouring
`DSH_DESKTOP_*` values so the suite reaches the welcome startup it covers.
* fix(plugin-manager): bound pnpm runs so a silent child cannot hold the profile lock
A pnpm child that prints its completion line and then never exits holds the
profile write lock until the host is restarted, so every other session and host
on that profile fails after its own lock timeout (issue #4981). execa resolves
its promise only once the piped stdio has ended, so a run now completes on the
process's own exit and drains the pipes under a bound instead of awaiting their
end.
A service run whose captured output stays silent for idleTimeoutMs is
terminated, reports timedOut alongside its exit status, and is not retried on
the next registry; a run with inherited descriptors captures nothing and is
left unbounded for its operator.
* test(plugin-manager): type the mocked child and await the drain descendants
The inherited-descriptor fixture no longer needs its legacy `as unknown` cast,
because the mocked child is typed for the fields the operations read; the
retired baseline entry is pruned (verify-no-unknown-casts).
The drain specs wait for the killed descendant to exit before removing its
directory: Windows refuses to delete a directory a live process still holds.
* test(plugin-manager): stop the drain descendant deterministically
The descendant that holds the pipes now ends on a stop file the teardown
writes, and on its own after a minute, so the fixture leaves no process behind
when a kill cannot be delivered. Windows stops it through taskkill, and the
spec waits for the exit before removing the directory it ran in.
* test(plugin-manager): run the drain descendant from its own script file
Windows cannot carry a nested `-e` script through another spawn argument: the
descendant failed to start there, so the fixture asserted on a process that was
already gone. The descendant now runs from a script file the fixture writes.
* test(plugin-manager): cover the bounded drain everywhere and stage the real descendant on POSIX
Windows does not stage a descendant that keeps the inherited pipes open past
the direct child's exit, so the real-process cases run on POSIX only. The
mocked child now models execa's own completion rule — its promise settles only
once both pipes end — which covers the bounded drain on every platform and
still fails without the raw-exit race.
* test(plugin-manager): cover the drain failure path without a child process
An output consumer failure that the bounded drain cuts short is staged with the
mocked child as well, so the per-file coverage gate also holds where the
real-process cases are skipped.
* fix(plugin-manager): terminate the whole tree of a stalled run
A lifecycle script is a child of pnpm, so signalling pnpm alone left an
approved postinstall writing into the profile after the operation reported a
timeout and released the lock. A stalled run now stops its process group (or
Windows process tree) and waits for it to disappear before the caller restores
the files. The cut tail's close is told apart from a reading failure by where it
happened, so a failure from before the cut still surfaces.
* docs(plugin-manager): regenerate the config catalog after the rebase
The English catalog gains `idleTimeoutMs` from the regenerator, and the Chinese
counterpart carries the same interface line so the pair stays consistent.
* fix(plugin-manager): terminate a run's tree through execa, keeping exit cleanup
Spawning a service run with `detached` made execa skip the handler that kills
the child when the manager exits, so a crashed manager left the pnpm tree
running in the profile. `killDescendants` spawns the run as its own process
group while keeping that cleanup, and it addresses the tree on every kill path,
including cancellation. The module now only decides the probe target and waits
for quiescence.
`prepareDevelopmentProject()` resolved the build target from the host
platform and architecture, so a Linux host threw `unsupported target
linux-x64` while writing the runtime descriptor. Pass the target in
instead: the launcher supplies the target it prepared, and isolated tests
supply a fixed supported target.
Extract `desktopTargetPlatform()` into desktop-build-paths.mjs so the
target-to-payload mapping has one home, and use it for the qualification
runner, which previously derived both its target directory and its
descriptor from the host architecture.
Correct the README limitation to name the module that now performs the
architecture check.
pnpm install --lockfile-only, run to align the new workspace edge's specifiers, also re-resolved unrelated entries: it pinned registry.npmjs.org tarball URLs on the two libreoffice-kit optional platform packages and downgraded glob 7.2.3 to 7.2.0. The tarball URLs fail verify-lockfile-policy against the mirror's published metadata.
Rebuild the file as the base lockfile plus only the three intended importer additions (the new package's block and its two consumers), so the resolution entries stay byte-identical to master.
The Team panel opens on hover with the subagent-catalog grace timings, pins
on click, polls the one-shot view RPC every 20s while open (replacing the
toolbar refresh/close controls), and eagerly loads on mount so the trigger
keeps a stable two-person icon. The lead card navigates from teammate
conversations, the current conversation carries a Current chat tag (selected
ring only among multiple members), task descriptions clamp to two lines
behind an expand toggle, an empty board shows a one-line description, and a
lone member narrows the panel to one column.
The header title row becomes an inline-size container: the preset label
hides first, then the Team trigger collapses to its icon and switches to
click-only opening. The subagent catalog moves to the front of the actions
band so lineage stays beside the title crumbs, and its pinned-menu behavior
matches the Team panel. Tooltip gains a gap prop for tight anchors.
Review follow-up on the tooltip layering fix. The helper probes
`bubble.contains(hit)` instead of identity, so a bubble that grows child
elements still passes, and each probe returns the hit element's tag and
class, so a regression reports what covered the bubble instead of a bare
`expected false to be true` — the pre-fix run now names `DIV.u82K-q_input`
for both probes.
The `portal` JSDoc says the ancestor stacking context confines the
bubble's z-index to that context rather than resolving it against
siblings.
The Summary narrated the change ("the suffixes the old read table recognized keep their historical value") rather than the contract, and at 109 words it exceeded the 100-word package Summary limit.
State the rule a reader at HEAD can act on: a suffix a recorded session already holds keeps its persisted value, every other suffix takes its language's short name. The dropped path-separator and leading-dot detail stays documented under Language selection. Both languages updated and the pairing record re-recorded.
The package joined the workspace as version 0.1.6-alpha.2 while the root release moved on, which failed three CI jobs from one cause: constraints (version must match root), Dependency layout (verify-npm-install-layout finds no workspace version), and Pack npm tarballs (dsh release members must share one version).
The open paragraph accepted Explorer's exit code 1 as a delegated handoff but
did not repeat the reveal paragraph's warning that the acknowledgement proves no
application opened the file, which the Agent Note records for the Remote answer
`{ opened: true }`.
The goal strip and the queue dock ride directly above the input card, and
both create a stacking context (`isolation: isolate`) so their frosted
panel stays behind their own content. A tooltip bubble left inside that
context resolves its z-index there, where the card — painted later in the
same context — wins: the goal strip's bubble showed a two-pixel sliver
and the queue dock's stayed entirely hidden.
Portal those dock tooltips to the body, where the bubble's z-index
resolves above the whole card. The ui-primitives `portal` contract now
names the capping stacking context beside clipped containers, because
neither container clips its bubble.
The queue-actions aria goldens lose the tooltip line: the captured center
column no longer contains the portaled bubble. The scenario still covers
it by role and adds a hit test at the bubble's center and bottom edge, as
does goal-bar.
The new JSDoc and test prose narrated the change instead of the shipped
behavior: "pre-unification", "the old read table", "used to persist ... it
now persists". A reader at HEAD cannot resolve those stages, and the
persisted-value rule they describe is a durability contract that stands on
its own.
Restate each as the present contract: a suffix whose value a recorded
session already holds keeps it byte-identical; every other suffix takes its
language's short name. Rename the test's transcribed expectation away from
the branch vantage and re-wrap the module comment the earlier edit left
over-long.
`package-target.ts` imported `macos-notarization-proxy.ts`, whose top-level
import of `@deepseek-ai/node-addon-system/flock` resolves through the package
`exports` to the unbuilt `lib/flock.js` on a clean checkout. The step that
emits that file runs inside the same packaging script, so `check:package`,
`package:*`, and the `restore:mac-proxy` recovery command all failed with
`ERR_MODULE_NOT_FOUND` before reaching it; running `pnpm run typecheck` first
left the host addon binary missing instead, which failed at lock time.
The proxy lock now loads the entry at first use through `flock-entry.ts`. When
the import is unresolvable or the host platform package lacks a binary listed
in its `prebuilds.json`, the loader runs `pnpm run build:native-system` and
`pnpm --dir native/system run build:ts` before importing again; other errors
and build failures propagate, and a failed load is retried by the next call.
`check:package` no longer touches the entry at all.
Tests cover the loader steps and load the proxy module from a directory where
the addon package cannot be resolved. The Desktop README records the
behavior in both languages.
(cherry picked from commit 2d74cf50310d4efbbccbbe79a105fcc3a468b19d)
On Windows 11 ARM64 build 26200 Explorer splits its own command line at
commas and equals signs and rejects percent-encoded non-ASCII in a file
URI, falling back to the Documents folder in both cases without reporting
it. `explorerTarget()` now escapes `=` next to `,` and decodes Node's
non-ASCII escapes back to the literal characters the shell resolves, so
the two rules stay in one place. The Agent Note and the target-encoding
table record the measured behavior.
The packaged-imports plugin moves from generateBundle to moduleParsed so
external dynamic import() and require() targets, which chunk metadata
omits, fail the bundle too. Each bundle now carries an import policy:
the main process allows Node builtins, sandboxed preloads only the
modules Electron's require polyfill resolves, and the failure names the
importing module and the allowed set. The wine Windows gate bundles
apps/desktop after the Host tsdown pass like build:lib:host, the
ts-build-config note lists that step, and the tsdown docs name the
workspace match exactly.
Fixes#4993
Review follow-up: the caption menu is inert while the Platform view is open,
which the previous geometry hid; state it under Known Limitations, and drop
the duplicated 40px figure the Desktop owns. The style spec's rule lookup
anchors on the rule boundary and asserts with toContain.
The embedded Platform view renders its 48px return bar at the top of the
window. On Windows the Desktop-owned caption menu occupies that strip (fixed
at top 0, 40px high, z-index 1100), so the "Back to DeepSeek Harness" label
and the Application/Edit menu text were painted over each other.
Offset the overlay by --dsh-windows-titlebar-height where the Desktop preload
publishes its caption marker, the same clearance AppFrame and the sidebar
caption controls use. The return bar and the native view bounds, which come
from the viewport element's rectangle, both move below the strip.
Point selectable hover content at HoverCard (Tooltip has no interactive
prop), require a persistently mounted toast host, scope the font-weight
cap to new feature CSS, prefer theme typography tokens, link the styling
owners, date the design-reviewer roster, add the scrollbar containment
rule, and make the skill discoverable from packages/client/AGENTS.md.
Four focus rings added on master after this branch's base still declared their
own colour: PresetGuideDialog's panel, CodeCard's action (state-business-primary),
SegmentedControl's tab, and AccountSection's link button (button-primary-fill).
The stylesheet corpus spec now rejects each of them, so they read
`--dsw-focus-ring-color` with the brand-token fallback like every other ring.
CodeCard keeps its 1px dense width; the other three take
`--dsw-focus-ring-width`.
Conflicts and how they were resolved:
- ui-agent-preset/AgentPresetSection.module.css: master replaced the icon-only
tooltip actions (`.iconButton`, `.iconDanger`, `.revealedPath`) with
`.cardHelp` + `.helpButton`, which deleted the one declaration this branch
edited. Took master's structure and re-applied the branch's change to the two
`.cardMain:focus-visible` rings, which master kept unchanged.
- ui-theme/README.md and README.zh.md: master documented the separately exported
`brand-font.css`; this branch documented `focus.css`. Kept both, with the
dynamic entry's sheet list at seven sheets including `focus.css`.
`README.i18n.yaml` re-recorded with `verify-translation-pairing --write`.
The recorded exit code 0 after about 30 seconds was not reproduced: an SSH
session 0 on Windows 11 ARM64 build 26200 measured exit code 1 after about a
second with no association invoked, the same exit code a delegated open
returns. The README, both Agent Note languages, and the `explorerTarget` JSDoc
now record that measurement, and the `Invoke-Item` contrast keeps its
qualitative part without the timing.
Pointer suppression previously cleared `outline-color` on the focused element
alone, so a ring painted by a descendant, a pseudo-element, or a `box-shadow`
stayed visible, and editable text controls lost the focus feedback they need on
click.
Ring colour now flows through `--dsw-focus-ring-color`, which component outlines
and focus-ring shadows read alongside the brand token. Every ring that inherits
the variable is suppressed together, and `body :focus-visible:not(:read-write)`
limits the suppression to rings while editable controls keep their own feedback.
Keyboard modality returns on Tab, Home, End, PageUp, or PageDown, and on focus
reaching a different control after a non-composing key. IME composition keys,
refocusing the same control, and window blur leave rings silent. The desktop
preload menubar suppresses its own ring inside its shadow root, where the
document-level attribute does not reach.
A verbatim `\\?\` prefix and a `\\.\` device namespace take Node's UNC
resolution, and the comma encoding has to survive them. The drive-root case is
pinned separately because Node restores a trailing separator against the host's
`path.sep`, which differs on the Windows host this opener runs on.
The root tsdown workspace pass builds every member concurrently, so the
Desktop main bundle could resolve its inlined workspace devDependencies
(dsh-home-paths, dsh-app-boot, dsh-deepseek-account) before their lib/
output existed. rolldown then kept them as external imports, which the
asar does not ship, and the packaged application failed at launch with
ERR_MODULE_NOT_FOUND on a clean checkout; stale lib/ output on long-lived
checkouts hid the defect.
apps/desktop leaves the root workspace list and build:lib:host bundles it
after the Host pass. Every Desktop bundle now runs a rolldown plugin that
fails the build when a chunk imports a bare specifier the packaged
application cannot resolve: electron and the manifest dependencies for
the main bundle, electron alone for sandboxed preloads.
Collects the standing judgment rules for product-user-visible GUI work:
token and shared-treatment reuse, light/dark readability, font weight and
size scale, feedback-surface choice (toast vs in-place notice vs empty
state), loading treatments, overlay dismiss/fit/clip checks, macOS/Windows
window adaptation, and spacing review. dsh-code-review links it from its
sources of truth so UI PR reviews load it.
- Agent Notes: cross-link the new decision with the native fatal recovery
and fail-loud release notes both ways, rewrite the older note's
console-retention and dialog-detail facts in place, and state the report's
actual bounds.
- crash-report: cut the rendered error section at 256 KiB and prune only
files matching the generated crash-<time>-<source>.log syntax.
- main: backendReady follows the backend state except during shutdown, so a
failure while tearing down a ready backend still reads as running.
- subprocess-local: the temporary-file-cleaner hint is appended only for
ENOENT; other codes rely on the appended error.
- client-modules: describeError is module-private; BootModuleRow.url's
JSDoc names its fallback role.
The ui-chat README described the visibility rule as "while the built-in browser
is available", a phrase that now reads like an option label. Both languages name
the Sidebar Browser, matching the sentence that follows.
A Host startup failure reached the crash report as a fresh Error built from
a message string three hops away from the original: the Host sent only
error.message over the fatal IPC event, DesktopHostProcess wrapped it in a
new Error, desktopErrorState flattened it to text, and main.ts wrapped that
text again. Stack, cause, code, and path were gone, and the Host's stderr
tail arriving with the later exit was blocked by the first-failure latch.
The Host now sends its complete inspected error alongside the message
(bounded to 64 KiB; carried over IPC rather than stderr because the two
race and the shell reports the first failure it sees). DesktopHostProcess
keeps it on DesktopHostFatalError.diagnostic, the backend error state
carries the original failure object, main.ts reports that object, and the
crash report prints the Host diagnostic verbatim in its own section.
apps/desktop imports @deepseek-ai/dsh-api-gateway/stream-protocol, whose
import chain reaches @deepseek-ai/dsh-typert-protocol and then
@deepseek-ai/cordis. Both intermediate packages declare cordis as a
peerDependency and nothing satisfied it for the application closure, so
electron-builder omitted cordis from app.asar and the packaged main process
died before opening a window with ERR_MODULE_NOT_FOUND. A workspace checkout
resolves the peer through pnpm's own links, so development runs and the unit
lanes stayed green while every packaged build since efbc8e26b6 (2026-09-20)
failed to launch.
The lockfile additionally drops a stale duplicate [email protected] in favour of the
7.2.3 it already carries; any real pnpm install removes it.
Verified with a signed win-x64 installer built from this tree: app.asar now
ships cordis and @standard-schema/spec, and the installed application opens
its main window.
The Settings → General row for Chat link destinations now reads
网页链接默认打开方式 with the description 对话中网页链接的打开位置 and the option
labels 应用内侧边栏 and 默认浏览器. English keeps the title "Open chat links in" and
now reads "Choose where to open web links", "In-App Sidebar", and "Default
Browser".
The `ui-chat.linkOpening` setting, its values, persistence, visibility rule, and
open behavior are unchanged. The row spec, the clickable-link gallery scenario,
and the two package READMEs name the new copy; the bilingual pairing records were
re-confirmed for the README pairs.
Carry the user-scoped agent-experience guidance into the repository unchanged so
project sessions discover it beside the other repository skills. The file,
including its frontmatter, is byte-identical to the personal copy.
Skip the per-owner wake ledger entirely when no cap is configured.
Rewrite the 2026-08-11 Agent Note in place for the opt-in cap and add
a note recording why the default was removed. Keep `subagent`
untranslated in Chinese prose and drop change narration from the test.
Refs #4944
The Contact us action already stopped prefilling the signed-in UID, but a
configured contact form URL could still carry its own prefill_uid or hide_uid.
contactUrl now drops both query parameters unconditionally, with regression
coverage that starts from a form URL containing them.
The contact URL test also pins the shipped default form path, and the injected
contactUs documentation and the AccountProfile summary no longer describe a
support UID that is no longer sent.
* fix(web): resolve XLSX part relationships and namespace aliases
* fix(web): keep Excel browser scenario in the host test program
* fix(excel): preserve XML encodings and relocated workbook parts
* fix(excel): exclude installed launchers from dependency patch
* fix(excel): restore Strict and case-equivalent workbook previews
The account menu entry that opens the questionnaire is now labeled Feedback in
English and 意见反馈 in Chinese. The menu action, contact form URL and its
configuration options keep their existing names.
Expected menu snapshots and the bilingual README record the new copy.
`maxConsecutiveWakes` no longer defaults to 3. Unset, every background
job completion wakes its idle owner; the cap is opt-in.
With the default cap, a session chaining background commands and
one-shot subagents exhausted the budget after three wakes. The fourth
notice was injected into the next-step inbox without waking the owner,
the client showed nothing, and the session stalled until the user
typed something (dsh-external/issues#641).
Closesdsh-external/issues#641
The recorded-session scenario edited with a single-line value, so the ARIA
golden could not catch a return to a newline-stripping editor. Edit with a
two-line value and assert the model-bound user message keeps the line break;
with the previous input element the scenario fails on that assertion. Also
refresh the stale input cast and pin that Shift+Enter is not consumed.
The inline queue editor was a single-line input, and HTML strips newlines
from such a value: opening and saving a multi-line queued message silently
collapsed it into one line before delivery. Edit in a textarea that grows
with its content, capped at six lines; Enter saves, Shift+Enter breaks the
line, and Escape still cancels.
client-modules: a batch that already executed is never requested again,
even when its first importer was a row it did register — every executed
batch URL is remembered and a still-missing row goes straight to its
one-resource URL. The in-process module system test in test-support is
updated to the aggregated arrival message. describeError replaces three
inline non-Error ternaries and is covered.
subprocess-local: the spill path is published only after the exclusive
open succeeds, so a failed open (EEXIST on a planted entry included) can no
longer unlink a path this process did not create; a reporter that throws is
contained and written to stderr; logSpillFailure builds the owner reporter
for both the runtime and the SSH helper so the logging path is exercised
once instead of duplicated. Tests cover the runtime logger, the bare-caller
stderr default, EEXIST, and a throwing reporter.
desktop: a fatal failure during shutdown still writes its report; the
report phase follows the backend state so a failure during a restart is
'startup' again; writeReport is a required recovery operation and the
dialog tests await the write; the shell pid is labelled; the withheld
response-header list states why set-cookie is dropped.
app-boot, READMEs, Agent Note: "never resumes" is restated as control never
returning to the failed operation while the loop runs until release
settles; inspect output is bounded; incident inferences (who removed the
spill directory, which batch failure occurred) are written as inferences.
- Headline skips the archive path 7-Zip echoes before an open failure
(also when the path contains spaces) and prefers the operation line.
- Raw process statuses such as 0xC0000005 are kept and printed in hex
instead of being clamped to INT_MAX.
- The expanded panel excerpts the result and 7-Zip's own lines rather
than the report header.
- The saved report keeps up to 16 MiB of output, records any cut, and
trims a partial UTF-8 sequence before decoding.
- The details view no longer reads the UTF-8 log as ANSI; the test seam
define and the zero-padded GetTime order are documented; the smoke
asserts a year-first report name and normalizes -FrameLibrary.
- README names the real updater cache directory.
master grew a fourth drag owner after this branch: the Platform overlay's return
bar (ui-settings-account) declared `app-region: drag` on its own header. The
branch absorbs it into the contract — the header marks itself `data-window-drag`,
its own app-region drag/no-drag rules are deleted, the ui-theme manifest pins the
row and its 48px height, and the Agent Note's row list names it — so the tree
declares the drag surface in exactly two places again (the shell's mark rule and
the Windows caption row).
Earlier resolutions from the first merge stand: the dockkit strip keeps its mark,
the sidebar-right pulse stays deleted, ui-settings-general keeps upstream's
dsh-settings and this branch's react-dom, and tsconfig.host.json takes upstream's
`files: []`.
Checks: build, build:web, typecheck, test:gui, the ui-theme/base-styles gates, the
window-drag-coverage lane, doc-sync and translation pairing.
Conflict resolutions:
- ui-dockkit TabPanel: keep upstream's reworked tab strip and re-add the row's
`data-window-drag` mark on `css.tabStrip`.
- ui-sidebar-right SidebarRight.tsx/.module.css: keep upstream's retention
rewrite and re-delete its hand-written one-frame app-region pulse; the shell's
window-drag watcher owns recollection.
- ui-settings-general/package.json: keep upstream's `@deepseek-ai/dsh-settings`
dev dependency and this branch's `react-dom`.
- tsconfig.host.json: take upstream's `files: []`; client sources belong to the
client aggregate.
- pnpm-lock.yaml: master's lock plus only the react-dom importer entries.
The fatal recovery dialog showed the last eight lines of the error and
promised that the full diagnostic was in the Electron console, which a
packaged installation never surfaces. Two field failures were diagnosed
from screenshots of those eight lines: a Host killed by an uncaught
exception whose path happened to be in the tail, and a renderer boot
failure whose real import error lived only in the renderer console.
Every reportFatal caller now names its source (host, web-boot, renderer,
main). Before the first dialog opens, the shell writes
crash-<time>-<source>.log under app.getPath('logs') with the source, the
startup or running phase, application and runtime versions, the complete
inspected error with its properties and cause chain, and the primary
window's recent error-level console output (a 64 KiB tail captured from
webContents 'console-message'). The dialog names the file on its own line,
independent of whether the error was shortened and in the listener-conflict
variant too; the wait is bounded to one second and a failed write shows the
dialog without a path. Startup keeps the ten newest reports. Buttons and
recovery actions are unchanged.
forwardWebRequest relayed transfer-encoding, connection, keep-alive, and
the other hop-by-hop headers from the Host's response into the dsh-app://
response, describing a Node fetch connection that Chromium never saw. It
also relayed the Host's `cache-control: public, max-age=31536000,
immutable` on plugin bundles whose revision changes on every launch, so
each start wrote another unreusable copy into the renderer's disk cache.
The forwarder now strips the connection-level header set alongside the
encoding headers it already removed and marks /plugins/ responses
no-store. Whether either header contributed to the observed batch load
failure is unverified; both are corrections in their own right.
One application batch is a single classic script that registers every
package in sequence, requested exactly once during boot. A transient
failure of that one <script> request failed every row in the batch and,
through arriveGraphRow's dependency wait, every row that injects or
requests them; the boot audit then listed each as "import failed (see
console for the import error)" with the real error only in the renderer
console.
Arrival now distinguishes the two failure kinds. A transport failure
(script error event, nothing executed) is retried once on the same URL and
the retry is shared by every row waiting on that batch. A script that
loaded without registering a row is never re-executed, because register()
rejects duplicates and a replay would stop at the first package the
original run did register. Either way each still-missing row then loads its
own one-resource combo URL, which the Host serves for every package. A batch
URL that failed is remembered so later rows skip straight to their
fallback; a one-resource URL is not, so it stays retryable on the next
import as before. Dependency failures are wrapped with the consumer and
dependency names so a cascade reads as a chain.
The module system records the last import() or prefetch() failure per
graph row at the outer operation, so factory execution errors are captured
as well as arrival errors, and clears it on success or invalidation.
assertEntriesActive takes the module system and reports that text per
fiberless entry.
The Desktop Host and every other profile launcher installed a fail-loud
handler for unhandled rejections only. A synchronous throw from a stream
'data' listener fell through to Node's default uncaught-exception path: it
printed and exited without the surface's release hook, and the label and
format differed from every other fatal diagnostic.
installFailLoud now registers the same report for 'uncaughtException' with
the label `fatal uncaught exception`, shares the exit latch and release
timeout with rejections, and never resumes the process. The diagnostic is
util.inspect(err, { depth: 4 }) instead of err.stack so enumerable
properties (a node:fs error's code, syscall, and path) and the cause chain
reach stderr; the `fatal load failure` label for rejections is unchanged
because the Web profile expected-output e2e tests match it.
OutputCollector.spillAll ran openSync/writeSync inside the stream 'data'
listener with no containment. When the private per-process spill directory
had been removed by a temporary-file cleaner, the first overflowing chunk
threw ENOENT from Pipe.onStreamRead, became an uncaught exception in the
Desktop Host, and the Electron shell reported it as a fatal failure.
Spilling is best-effort by contract (spillPath is optional; a failed final
close already withholds it). Every filesystem failure while opening or
appending the spill file now discards the spill, reports once through the
owner's logger, and leaves in-memory collection untouched. Spill inputs move
into one SpillOptions object with the reporter, so tail-only collectors no
longer pass an empty directory; prepareManagedProcessBinding is the explicit
resolve step for the directory and reporter defaults, and the runtime and
SSH helper supply their plugin loggers.
Tests cover ENOENT on open (removed directory), ENOTDIR on open, and ENOSPC
on append after the file exists, asserting the tail stays byte-exact, the
spill file is withdrawn, and the reporter fires once.
The installer showed "Failed to decompress files" and discarded 7-Zip's
error output, so a failure such as security software quarantining an
extracted file could not be told apart from a full disk or a broken
download.
- InstallerExtract returns negated Win32 errors for launch failures so
they no longer collide with 7-Zip exit codes; 7-Zip writes UTF-8 errors.
- window-frame.dll composes a report (result, archive, destination,
Windows version, full 7-Zip output), saves it under the updater cache
directory, and presents a task dialog with the first error line, a
collapsible bounded excerpt, and a Copy error details button. Silent
installs keep only the report; a missing task dialog falls back to a
message box.
- Unsigned Windows builds name their installer *-unsigned.exe so a
shared file cannot pass for a release artifact; version discovery
still counts them.
- The directory smoke asserts the saved report on a broken archive and
smoke-windows.ps1 accepts the prepared DLL.
Refs #4893
Opened and copied authorization links carried no palette, so the Platform
login page rendered its own stored preference. Append the effective Desktop
palette as theme=light or theme=dark, resolving system through the theme
source the preload publishes.
The theme source now reaches nativeTheme on Windows as well: the preload
drops its macOS-only guard, since shouldUseDarkColors is read for every
platform, and gates the DOMContentLoaded listener with once.
master replaced the startup-bound messages constant with a locale that
follows the settings preference; the About dialog now reads the same
locale as the update prompts so the two never mix languages.
The Contact us entry now opens the current Feishu questionnaire, and the URL
no longer carries the signed-in account UID: contactUs stops reading the
account profile, contactUrl stops writing prefill_uid and hide_uid, and the
remaining build and browser context fields keep their existing behavior.
The url, package tests and the bilingual README and account-login note state
the same current facts.
The declarative-preset refactor (#4569) removed the registry's read Remote
together with the YAML editor, and with it the 查看配置 card action that
opened a preset's composition read-only. #4850 brought the Creator entry
back and had to leave 查看配置 out because `agentPresets/read` was gone.
Give the registry a read-only `read` Remote that renders a declaration's
child plugin list from the in-memory definition in the Loader's own
entry-list dialect, so `!!js` conditions read as declared, and put the card
action back at the right end of each card footer: an icon that opens the
YAML in a read-only dialog titled after the preset. Nothing accepts YAML
back, and a broken preset stays readable because its diagnostic points into
that YAML.
Fixes#4895
* fix(session): repair interrupted turns during V3 migration
* docs(session-format-v3-to-v4): state reference remapping in the summary and code
The frontmatter summary still promised preserved references after the edge
began remapping audited same-artifact references around an inserted
interrupted turn/end. Two local comments record why the opaque branch moves
only `seq` and why the open-step flag needs no reset at a turn boundary.
* docs(session-format-catalog): describe the V3-to-V4 edge as a converting link
The catalog listed the edge as an identity conversion, which stopped being
true once it inserted an evidenced interrupted turn/end and remapped the
audited local references that follow.
The settings sheet's Open configuration file action opened nothing on Windows when the user's default lived only in the Explorer per-user record (FileExts\\<ext>\\UserChoiceLatest), which Explorer honors and the association resolver inside the host process does not. Invoke-Item also exits 0 when it finds no application, so the Remote answered { opened: true } and the sheet rendered no error.
Windows opens now hand the path to explorer.exe as one argv element - the resolution a double-click performs - and revealNativePath shares that invocation rule. The shared helper accepts Explorer's delegate exit 1 for an open and a reveal alike; every other failure and every cancellation still rejects. The PowerShell command string and powershellLiteral are gone: the path crosses the process boundary as argv, with no escaping step.
Fixes#4428
Route file, terminal, browser, split, fullscreen, close, and refresh commands to the focused panel. Connect native Desktop page and window actions to the same command owners.
Co-authored-by: 神河一号 - 烈焰流光 <[email protected]>
Add the keyboard registry, device-local preferences, and searchable shortcut editor. Support Web routing and native Desktop single-key and two-key bindings.
Co-authored-by: 神河一号 - 烈焰流光 <[email protected]>
* docs(agent-preset): enumerate and verify skill files with Host file tools only
* docs(agent-preset): list template files instead of enumerating the skill directory
The glob and search tools spawn a native ripgrep process, which cannot read
inside app.asar any more than a shell can; the skill now carries the complete
template file list and forbids enumeration.
* feat(tool-cordis): inspect live plugin Config schemas
Replace the Host `Builtin` inspect provider, which described a dynamic Host
half the model can no longer define, with a `Config` provider that lists every
live Loader entry with its Config status and projects one entry's native
Schemastery Config into a self-contained JSON Schema document. app-boot exports
its projector, native-schema check, and the shared `loaderExpression`
definition for the live walk; the CLI dump keeps its profile-composition path.
* fix(tool-cordis): refresh config catalog pointer and presets e2e provider list
* feat(agent-preset): progressive creator skills and profile shell facts
Split `cordis-plugin-development` into a short procedure with `references/`
read on demand and copyable `templates/`, replace the source-reading ban in
both creator skills with an ordered list of knowledge sources, and add the
`cordis-composition-reference` skill with the Loader patch dialect and a
generated, doc-sync-gated list of loadable plugin packages. `shell-env`
exposes `DSH_PROFILE` and `DSH_PROFILE_DIR` so skills can address the installed
profile.
* chore(agent-preset): regenerate plugin package list after merging master
* fix(tool-cordis): resolve the Loader at Config query time
A Host composed programmatically without a Loader keeps registering the
inspect providers; `Config.listConfigs` reports the missing Loader when
queried. Refresh the advanced-toolchain recordings whose provider list now
carries `Config` in place of `Builtin`.
* chore(agent-preset): regenerate plugin package list after merging master
* docs: regenerate module graph for the tool-cordis app-boot dependency
* docs: regenerate module graph for the shell-env app-boot dependency
* docs: regenerate event graph pointer for app-boot exports
* fix(agent-preset): address review on creator skills
`Config.listConfigs` reports each entry's resolved `packageDir` through the
profile package lookup, and the skills read READMEs and built declarations
there instead of guessing `$DSH_PROFILE_DIR/node_modules`, which holds only
profile-installed packages. Skills name the PowerShell variable syntax and the
no-profile omission; the composition reference no longer attributes
`overridden` to `list_plugins`; references point at the templates instead of
pasting them. The skills spec renders every skill and bounds it by the pruner
threshold in code points, checks referenced files and the source-reading ban
across all skill files.
* fix(tool-cordis): address review on the Config inspect provider
Report the Loader entry id (unique across include trees) beside the tree-local
patch id, classify group and include carriers as `tree`, treat disabled and
disposed fibers as `inactive`, and drop the unreachable boolean-schema branch
by narrowing the projector's root type. The presets e2e now queries
`Config.listConfigs` over the booted Web profile and projects the shipped
`tools` row; the advanced-toolchain recordings carry the updated manifest.
* fix(tool-cordis): page the Config directory and filter it by plugin name
A full directory of a shipped Web profile exceeds the 50000-byte spill cap
once rows carry more fields, and the spilled preview is no longer JSON.
`Config.listConfigs` now returns pages (`offset`, `limit` up to 100, default
25) with `total` and `nextOffset`, and an exact plugin `name` filter; the
presets e2e reads the shipped rows through the filter.
* chore(agent-preset): refresh skill recordings after the paged Config query
* docs(agent-preset): teach the composition skill to migrate legacy presets
A preset authored before declaration rows lived in
`$DSH_HOME/.agent-presets/<id>/` as `preset.yml` plus `agent.cordis.yml`;
the skill now maps those files onto a declaration bundle and re-checks plugin
names against the package list.
* docs(agent-preset): read skill files through the file-read tool only
Desktop ships the skill directory inside app.asar, which the Host process can
open but shell commands, node, and pnpm cannot. The skills now say to read
references and templates with the file-read tool and copy templates into the
workspace before installing or checking them; the spec rejects shell reads of
skill files.
* docs(agent-preset): document bundle display metadata and package.json icon in the host-plugin reference
The consolidated commit refreshed unrelated transitive resolutions while it
resolved the settings portal's new react-dom dependency. Restore those entries to
their previous versions and keep only the two importer entries the portal needs;
the lockfile already carries both snapshots.
The declarative-preset refactor replaced the settings section's visible
"让 Agent 帮我创建预设模式" entry with a question-mark icon beside the title
that opened an explanatory modal, and rewrote the section's markup and CSS
along with it.
Restore the section as Ziya's commit 1ed59986f7 had it, minus the copy
(复制预设) and view (查看配置) card actions the refactor removed for good:
the roster keeps its cards, mode help and picker row, the custom group stays
on screen with the Creator entry as its tail, and the question-mark modal is
gone. The creator button stages the cordis preset and starts a task in it,
disabled with the picker's own reason while mode selection is off.
Delete and open-location stay out: the registry exposes only `list` and
`select`, and the roster row no longer carries a trust or a document.
The section spec covers the entry's four paths, and the Web scenario
replaces its help-dialog step with one that drives the entry to a host
session composed from cordis.
* feat(tool-cordis): inspect live plugin Config schemas
Replace the Host `Builtin` inspect provider, which described a dynamic Host
half the model can no longer define, with a `Config` provider that lists every
live Loader entry with its Config status and projects one entry's native
Schemastery Config into a self-contained JSON Schema document. app-boot exports
its projector, native-schema check, and the shared `loaderExpression`
definition for the live walk; the CLI dump keeps its profile-composition path.
* fix(tool-cordis): refresh config catalog pointer and presets e2e provider list
* fix(tool-cordis): resolve the Loader at Config query time
A Host composed programmatically without a Loader keeps registering the
inspect providers; `Config.listConfigs` reports the missing Loader when
queried. Refresh the advanced-toolchain recordings whose provider list now
carries `Config` in place of `Builtin`.
* docs: regenerate module graph for the tool-cordis app-boot dependency
* docs: regenerate event graph pointer for app-boot exports
* fix(tool-cordis): address review on the Config inspect provider
Report the Loader entry id (unique across include trees) beside the tree-local
patch id, classify group and include carriers as `tree`, treat disabled and
disposed fibers as `inactive`, and drop the unreachable boolean-schema branch
by narrowing the projector's root type. The presets e2e now queries
`Config.listConfigs` over the booted Web profile and projects the shipped
`tools` row; the advanced-toolchain recordings carry the updated manifest.
* fix(tool-cordis): page the Config directory and filter it by plugin name
A full directory of a shipped Web profile exceeds the 50000-byte spill cap
once rows carry more fields, and the spilled preview is no longer JSON.
`Config.listConfigs` now returns pages (`offset`, `limit` up to 100, default
25) with `total` and `nextOffset`, and an exact plugin `name` filter; the
presets e2e reads the shipped rows through the filter.
The declarative-preset refactor replaced the settings section's visible
"让 Agent 帮我创建预设模式" entry with a question-mark icon beside the title
that opened an explanatory modal, and rewrote the section's markup and CSS
along with it.
Restore the section as Ziya's commit 1ed59986f7 had it, minus the copy
(复制预设) and view (查看配置) card actions the refactor removed for good:
the roster keeps its cards, mode help and picker row, the custom group stays
on screen with the Creator entry as its tail, and the question-mark modal is
gone. The creator button stages the cordis preset and starts a task in it,
disabled with the picker's own reason while mode selection is off.
Delete and open-location stay out: the registry exposes only `list` and
`select`, and the roster row no longer carries a trust or a document.
The section spec covers the entry's four paths, and the Web scenario
replaces its help-dialog step with one that drives the entry to a host
session composed from cordis.
Review pointed out that About read its copy from the renderer-reported
Windows language while the shared update dialog stamps its lang and
close label from the startup locale, so an in-app language switch mixed
languages inside one dialog. About now uses the same startup messages
as Check for Updates. The menu click also logs a dialog that fails to
open instead of leaving an unhandled rejection, and the return type no
longer hides the message-box result behind unknown.
* feat(workspace): stop a Session's running work before archiving it
Archiving was a pure visibility write, so a Session archived mid-turn kept
running out of sight. The Workspace registry now owns archive admission
through two Host events: `workspace/session-activity` reports what still
runs and refuses the plain archive as `workspace/session-active`;
`workspace/session-stop`, requested with `stopActivity`, stops that work the
way the user's own stop actions do before the archive write. The API Session
Controller reports and stops turns, subagent descendants, and owned jobs and
rejects any model step for an archived Session; the Schedule plugin reports
and deletes active reminders. The sidebar turns the refusal into a
stop-and-archive confirmation listing the Host's answer.
* test(workspace): pin the widened archive signature and prove child liveness by heartbeat
The Client inspect catalog spec pins archiveSession's new options
parameter, and the archive e2e reads the Bash child's liveness from a
heartbeat file instead of a pid: the Linux sandbox runs the tool in its own
PID namespace, so the pid it reports is not comparable from the test.
* test(workspace): cover the archive-admission providers and dialog to the per-file gate
Drop the provider's unused disposer and the dialog's unreachable confirm
guard, read reminders without a catch that no composition can reach, and
add the cases the coverage lane found missing: a stop without a jobs
registry, a looping lineage, a descendant whose projections reject, the
dialog's Escape guard and non-Error reason, and a family merged by another
provider.
* fix(workspace): write the archive before stopping, gate subagent lineage, and delete reminders under the schedule barriers
Review round on #4765. The registry now writes the archive set before it
dispatches `workspace/session-stop`, so the durable set already gates
every wake the stops induce; the controller's `agent/pre-step` gate also
blocks subagent descendants of an archived Session and contains each
cancel or kill on its own. The Schedule plugin's stop listener runs as a
management delete in the agent's transaction queue between the same two
persistence barriers the `schedule_delete` tool uses. The confirm dialog
selects singular or plural English lines and keys list rows by index.
Docs: archive-admission section with type-equiv blocks on the workspace
subsystem page, write-first order and residual windows in the READMEs
and the Agent Note, and the missing zh reminders sentence.
* test(web): scope the question composer's New Session wait to the Sessions tree
The wide sidebar's New Session button carries the same visible label as
a fresh session row, so the unscoped `getByText` either settled on the
button before the row existed or hit a strict-mode violation once the
row rendered first, which is how the snapshots lane failed on
ece68c2ab5. Waiting inside the Sessions tree names the row alone.
* fix(schedule): answer archive admission from the owned runtime's fold, not the projection registry
The `sessionProjections.snapshot` API is the Client projection view and
is not a Host read path (review on #4765). `ScheduleRuntime.activeRecords()`
folds the live log the way every drive does, and the archive-admission
listeners ask the owned runtime for that fold: a session without a live
agent or without an owned runtime has nothing armed to fire, so it reports
nothing and has nothing to stop. Covers the runtime-less agent, a fold that
turns unreadable at the persistence barrier, and the stopping/faulted
runtime answering nothing.
* refactor(subagent): report and stop subagent descendants from the Subagent runtime
The `subagent` family of archive admission moves out of the API Session
Controller into `dsh-subagent`, which records the lineage at delegation
and owns the descriptor (review on #4765): `SubagentRuntime` answers
`workspace/session-activity` with the running descendants, labelled from
each child's descriptor through a live Session observation, and
`workspace/session-stop` by cancelling each of them with the parent
cause. The controller keeps turns, owned jobs, and the lineage step gate,
and composes its provider through `ctx.plugin(SessionActivityProvider)`
with declared injections instead of a bare installer call. `dsh-workspace`
becomes an optional type-only peer of `dsh-subagent`.
* refactor(jobs): answer archive admission for owned jobs from the job registry seam
The `job` family moves out of the API Session Controller into `dsh-jobs`
(review on #4765): the seam's constructor installs the
`workspace/session-activity` and `workspace/session-stop` listeners for
every implementation through the abstract `list` and `kill` alone, so the
controller no longer knows the jobs vocabulary and keeps only the turn
family and the archived-lineage step gate. `dsh-workspace` becomes an
optional type-only peer of `dsh-jobs`.
* refactor(workspace): leave SessionActivityKindMap empty and let each provider merge its family
The workspace package no longer names any activity family (review on
#4765): `SessionActivityKindMap` is empty, and each provider merges its own
key from a module both its faces import — `turn` in the API Session
Controller's types, `job` in the job registry seam's view, `subagent` in
the Subagent runtime's control types, `schedule` in the Schedule plugin's
types. The confirm dialog imports those modules for its cases and keeps
the generic line for any other key; the registry and controller suites
merge test-owned families instead of borrowing the providers'.
* refactor(agent): report and stop the running turn from the Agent registry
The `turn` family moves out of the API Session Controller into `dsh-agent`
(review on #4765): the registry's constructor installs the
`workspace/session-activity` and `workspace/session-stop` listeners for
every Agent it publishes, answering with its own lookup and cancelling the
turn with the user cause, and merges the `turn` key from its client-safe
types. The controller keeps only the archived-lineage step gate, renamed
`ArchivedSessionGate`. `dsh-workspace` becomes an optional type-only peer
of `dsh-agent`; the workspace package depends on none of the providers,
so the edge stays acyclic.
Since #3127 a foreground Bash command is a registered job from its start,
so the archive dialog lists it beside the turn; the e2e and its ARIA golden
expect that row, and the capture keeps the running job by design.
The shared code-language table must not claim a suffix a dedicated viewer
declares, and the Host read card must keep the `lang` values it already
persisted.
- Drop `csv` from the shared table: Code registers before Spreadsheet, so
the earlier body would take `.csv` and the browser snapshot gate fails.
- Add `readLangHintForPath`, the read card's legacy projection over the
shared table: the 43 pre-unification suffixes keep their old short ids
byte for byte, new suffixes use the canonical id.
- Add an ownership regression test over the full preview registry that
turns red when `csv` returns, plus byte-level and alias-normalized tests
for the read projection.
The client UI i18n gate rejects literal message text in main.ts; the
Windows About dialog now reads the product name from the dictionaries
like every other shell string.
Three tools capped the preview text they return with String.prototype.slice
at a UTF-16 code-unit budget. A cut that lands inside a surrogate pair
leaves a lone high surrogate, which is not well-formed text: a strict JSON
reader rejects the durable Session log carrying it, and the preview shows a
broken glyph. A lone surrogate retained in history has already blocked every
later Messages request for one session.
Centralize the cap in output-retention as
truncateWithoutSplittingSurrogatePair, and use it from the persistent bash
and pwsh shells and the string-replace editor. The cut drops the unpaired
half instead of keeping it, so the retained text stays a prefix and is at
most one code unit shorter than before.
The new dependency edges and the shifted Config sources regenerate
docs/module-graph and docs/config-catalog, including the committed
translation pair.
Refs #4817
Electron's About role on Windows falls back to a bare system message
box, which looks nothing like the shell's dimmed update prompts. Design
asked for the version check to match Check for Updates, so the Windows
menu item now opens the shared update dialog with the application name,
localized version line and a single OK button; macOS keeps the native
panel. The About command follows the renderer's chosen language like
the rest of the Windows caption menu.
The document Code preview and the Host read card each owned an
extension-to-language table that had drifted: the preview table had 13
extensions the read card lacked, and the two returned different language
ids for the same suffix. Both now resolve through the new zero-dependency
@deepseek-ai/dsh-util-code-language, which also adds common Windows,
config, markup, and language extensions. Every added grammar stays in the
lazy set; the boot grammars are unchanged.
Fixes#4822
The Windows badge used a 44px radius on its 1024px plate, which reads as
nearly square on the desktop next to other Windows 11 icons. Raise the
radius to 192px (3/16 of the plate, matching Tencent Meeting's corner)
and re-export the transparent 1024×1024 PNG from the SVG; the ICO for
the app, installer and uninstaller and the About panel image all derive
from that PNG.
The manual check dialog explained that the release bundles its matching
dsh runtime before mentioning the restart. Users only need to know the
app restarts to finish updating; the bundling detail belongs in the
README, not the confirmation. Both locales carry the shorter sentence.
* feat(cli): dump native Cordis config schemas
* test(cli): include schema dump in launcher help snapshot
* feat(cli): export JSON Schema for Cordis configuration
* fix(cli): preserve config schema discovery and annotations
* fix(cli): conservatively project effectful config validation
* refactor(boot): encapsulate profile config schema generation
* fix(cli): follow Loader disabling and native writeback in config schema
- Dormant group/include rows (disabled without group: true) may omit
config and are recorded as childless trees; the entry schema skips
their child declarations, matching the Loader, which never creates them.
- Lazy builders run once per document, and a wrapped null-only projection
keeps a deduplicated type list so the document stays compilable.
- Union widening applies only when an earlier branch can write adapted
values back through a container; +Infinity max and -Infinity min
without a step are treated as the native no-ops they are.
- Projection limitations repeat at every entry sharing a Config, and the
entry schema documents the boolean/expression domain of disabled.
- Record the schema-dump validation policy as an Agent Note.
The bundle page now titles the fixture by its package name, prints the
row id as code under the row's title, and drops the missing-description
line, as master's row and official goldens already record.
The Plugins page spec's configuration bodies take the view positionally
as master's fixture does, the companion and section specs read a slot
entry's injected face through a Pick of the members they assert instead
of a double assertion, and the no-unknown-casts baseline drops the
entries retired with the moved settings tests.
The afterPack spec mocked the built runtime tree through a path relative
to the spec, which only resolves once `lib/` exists; on a clean checkout
the hook imported the real module and every case failed. It now mocks
the path the hook actually resolves, as the asar-unpack spec does, and
passes with `lib/` absent.
The toolchain probe assertion reached its matcher through an assertion
to unknown, which `verify-no-unknown-casts` forbids. It compares the
reported tools and detail directly instead.
Pinned directories are now opened with listing access so the handle takes
part in share checks and a rename or replacement of the directory under the
helper fails; a metadata-only handle did not participate. A DSH_HOME published
as an environment variable protects overlapping targets, and /KEEP_APP_DATA
retains data alongside --updated, matching the flags electron-builder passes
to an older uninstaller. The remover clears read-only attributes, keeps
deleting siblings after a locked file, and removes only ordinary empty scope
parents below %APPDATA% instead of an NSIS RMDir that would also unlink a
junction. The redundant un.DataInit extraction is gone.
The native regression now runs from the installer test driver and covers the
protected root, rename blocking, read-only and locked files and parent
removal; the uninstaller smoke uses a per-run scope, polls for the temp-copy
handoff, asserts InstallLocation after each silent install, and adds
/KEEP_APP_DATA and DSH_HOME-inside cases. The Agent Note keeps the topic's
2026-09-08 filename, states the amendment in the decision, and defers macOS.
A foreground shell command is a job while it runs, and its removal reaches
the browser one coalesced roster frame after the tool result, so a
conversation golden captured right after the turn could catch the header's
running-job control; the replay round-trip golden did on CI. captureStableAria
now waits for that control to leave unless a scenario keeps it, which the
two job scenarios that capture a running row do.
The composition suite's idleSilenceMs: 30000 was below the suite's own send
bound (dsh-terminal-bash timeoutMs: 300000), so the comment claiming silence
could settle nothing was wrong: a regressed send still settled as inferred_idle
after ~30 s and only the accumulated case timeout caught it.
The case now records every send's waitReason and requires at least six
stdin_read settlements with no inferred_idle. Reinstating the removed prompt
override fails it in 30 s with 'expected 0 to be greater than or equal to 6'
while the output assertions still pass. The silence tier returns to its product
default.
Also renames the two stale test titles ('private shell prompt', and the
cancellation phase that no longer covers an initialization send), pins the
stdin_read fallback's session retention, and records the interactive-child
consequence of that retention in the package README pair and the Agent Note.
The issue reported .ini files with no preview. On master the ini grammar is
already in CODE_HIGHLIGHT_EXTENSIONS, so the code renderer claims the
suffix and .ini previews. No production change is required; this test
records the admission behavior so a future allowlist change cannot silently
drop the reported suffix.
The static CI gate rejected four new assertions to unknown in the
rewritten Team panel specs. Build the Session list, Session snapshot,
and injected-callback fixtures as typed values, prune the retired
baseline fingerprints, and regenerate the module graph, config, and
persistence catalogs for the removed Remote dependency edges and the
shifted source lines.
The Host's default marker is best effort: on Windows it comes from the
Shell's ASSOCSTR_EXECUTABLE/APPID answer, which does not always name an
enumerated handler. When no application carried that marker the split
button promoted 显示文件位置 to the primary action even though the menu
listed applications that can open the file.
Select the OS-marked default when present, otherwise the first registered
handler in Shell preference order, and mark it as the menu default.
Reveal is promoted only when no application is registered at all.
Windows 11 ships some applications (Notepad, other Store apps) as MSIX
packages whose Shell handler reports an indirect icon reference such as
@{Microsoft.WindowsNotepad_...?ms-resource://.../NotepadAppList.png}
instead of a file path. SHDefExtractIcon cannot read that reference, so
the menu showed the generic fallback glyph.
Resolve an indirect reference with SHLoadIndirectString before extracting,
and fall back to the handler's executable when the resolved source still
yields no image.
Three comments in the same review round went unread and still applied.
A failed tag push left the local tag in place, so the printed recovery
`git tag … && git push …` short-circuited on "already exists" and never
pushed. The result now carries the commands still outstanding, and the
tests cover a failed push and a failed tag separately.
Automatic numbering caught every error from resolving the upload
destination, which turned a misconfigured destination into silent local
numbering that can reuse a published index. It now checks whether a
destination is configured at all and lets an invalid one fail, and the
remaining catch names the error it reports.
`pnpmCommand` fell back to a bare `pnpm`, which on Windows is the batch
shim spawnSync cannot start — the failure this branch exists to fix. It
says so instead, and keeps the fallback where a shim is executable.
The afterPack regression this branch introduced earlier reached review
because nothing exercised that hook: one spec calls it directly for an
ordinary build, a prepared qualification runtime, and a build version,
and it fails when the expected version collapses back to one value.
The Team panel read a Remote snapshot on open and behind an explicit
refresh button, while the subagent catalog panel updates from Host
projection frames. The Team projection was Host-only and its `apply`
mutated state in place, so the projection registry could never publish
a client view for it.
Publish a durable-only `agentTeam` wire view: roster rows with their
durable phase, non-deleted task views through the same readiness,
owner-name, and write-overlap derivation the task board returns to
agents, and `failure` beside the last valid state when a persisted Team
record is rejected. `apply` now returns a new state per applied Team
event and replaces only the touched collection, so mailbox-only events
reuse the previous view reference and publish nothing.
The Web panel derives every row from the shared Session store: the Lead
from the current Session's subagent address, running/inactive from
Session status, and the model from the member Session's durable
`modelSelection` projection. Opening requests the Lead's projection
baseline when the store has none, a failed read shows a retry control,
and the refresh button and Remote mount are gone. The `agentTeams/view`
Remote method remains for callers that need live availability and the
configured model.
The Team panel web e2e now asserts that Host-side task creation and
claiming reach the open panel without any user action.
The registry's owner fence is the only access rule for a human kill: a job
is killable by the session whose list carries it, and a child session's own
jobs are no exception. The fence had pulled two implementation helpers out
of the Session Controller's package entry (plus a safe-host-export exception
for them) so the job controller could import them; both go, and the job
controller no longer depends on the agent registry or the Session Controller.
* feat(preset): declare Agent compositions in profile YAML
* fix(preset): share scope binding cleanup and refresh module graph
* fix(preset): use host syntax colors in the YAML editor
* fix(preset): write new profile declarations as block YAML
* fix(preset): align eager activation with browser and worker surfaces
* fix(ui-agent-preset): retain the existing preset card layout
* fix(preset): keep rows waiting for a Host service mounted and re-audit after settlement
A preset row whose Host provider was still activating at declaration time
was reported as waiting for a missing service, and the registry recorded the
definition as broken for the rest of the process. Split the mount audit into
failed and pending rows: failures and root-realm leaks still reject the
mount, while pending rows stay mounted and every read and binding re-audits
them after the Host Loader tree settles.
Remove the accidental self-dependency of @deepseek-ai/dsh-web-app and its
lockfile self-link. Archive the copy-only preset authoring note, which the
declarative-preset note fully supersedes, and update the ralph note's
restore recipe to the profile-patch mechanism.
* chore(lint): re-key and prune the unknown-cast baseline after the master merge
* refactor(web-app): ship each Agent preset as its own bundle patch file
`dsh.bundle.patch` accepts an ordered file list, so the four shipped preset
declarations move out of the 1000-line web patch into
`presets/<id>.patch.yml`, one insert per file, applied after the host patch.
The host patch keeps the preset registry row. Gates and test scaffolds that
read the web layer resolve every patch file through `bundlePatchPaths`
instead of one hardcoded path.
* refactor(preset): drop the save notice injected into live Agents
The notice duplicated the save result the Web editor already shows, and
after the producer-owned message sources it would have needed its own
persisted source kind. Saving now only persists and reports the activation
diagnostic; the persistence catalog is unchanged from master.
* docs: regenerate the module graph after dropping the registry's llm dependency
* test(ui-agent-preset): cover the guide dialog's keyboard handling on the declarative section
* fix(preset): address the remaining review comments
Apply the lowercase id rule only when creating a declaration, so presets
declared with other ids stay editable. Export `isBuiltInPreset` from the
display fold and key the client card groups on it. Remove the dead `error`
and `retry` locale keys, the JSDoc orphaned by the `trust` removal, the
`trust` fields in the plugin-inventory test fixtures, and the stale
`dsh-agent-presets` package name in comments and READMEs.
* refactor(preset): make the Web preset page view-and-select only
Remove the child-plugin YAML editor, the `agent_preset` tool and the
registry's read/save Remote methods. Presets are declared and changed only by
bundle patches; the settings page keeps the roster, the new-task default and
the mode help, and its question icon explains that and offers to start a
Creator-mode task. The `editing-cordis-compositions` skill and the Creator
persona describe the bundle-based path, and the authored skill-load Session
and Web snapshots are refreshed for it.
* docs(skill): make the composition skill self-sufficient for preset authoring
State where the shipped declarations live, list every declaration field,
inline a complete minimal bundle and an override example, and name the
verification calls, so Creator mode no longer reads DSH package sources to
infer the format. Refresh the authored skill-load Session for the new text.
* test(tools): drop the removed agent_preset tool from the catalog expectation
Nobody reads the uninstaller details list and NSIS keeps no log file, so the
failure message added no diagnostic value while costing a bilingual string.
Removal failures now leave residue silently; the uninstaller carries no custom
copy.
The import came with the session.killJob Remote that later moved to the
job controller; nothing in this package reads ctx.jobs any more, and the
package declares no dependency on the seam.
Review found that `apps/desktop/README.md#release-versions` and the
2026-09-16 version-derivation note already specify this convention, and
that three of their requirements were reversed without saying so. The
rules themselves are right; what was wrong was leaving them undocumented
and rejecting one of the forms they prescribe.
A stable base takes `-test.YYYYMMDD.index`, which validation refused
because such a version ranks below its base. That ordering is the point:
the form belongs to a test feed that never serves the production
release. Both READMEs, the older note, and a new note now record which
requirements this change reverses and why.
Three defects the review found, each reproducible:
- `afterPack` compared the bundled runtime against the product version,
which broke installed-update qualification: that flow passes a runtime
whose version was deliberately rewritten. The expected version is now
a parameter the preparer supplies, and `verifyDesktopRuntime` says
"bundled X is not the expected Y" rather than naming Electron.
- Upload could only learn the published version from a shell variable,
conflicting with "release fields come only from the target file". It
reads the completion record packaging wrote, and a mismatch now prints
the version that record holds.
- Automatic numbering read one listing page and one fixed directory. It
now follows every page, discards a listing it could not finish rather
than reusing an index that would overwrite a published installer,
bounds the request through the client's own timeout, and reads the
directory an unsigned build actually writes.
Also: `npm run` and `yarn run` set `npm_execpath` too, so the pnpm
resolution checks the path names pnpm; the argument separator can arrive
after a run script's preset arguments, not only first; and the toolchain
probe, parser, build commit, and tarball readers now have specs.
tool-pwsh-persistent installs its own `prompt` function after spawn, so
terminal-bash prompt readiness never matched and every send degraded to the
silence tier (idleSilenceMs + handoffGraceMs) under production defaults. This
is the defect the persistent bash tool fixed for the bash stack.
The tool now initializes without a prompt send and detects the no-end-marker
fallback through the seam's stdin_read wait reason instead of matching its own
prompt text. The private prompt constant and its stripping are deleted, so
partial output can carry the backend's own trailing prompt.
Tool calls drop from 8493/3722/3832/3759 ms to 1340/255/251/241 ms (spawn and
initialization plus the first command, then three warm commands; Windows,
production defaults, real Loader composition). The loader composition suite
pushes idleSilenceMs beyond the send bound, so every case fails if prompt
readiness regresses.
On Windows 11 the Start menu context-menu Uninstall opens the installed-apps
list for every Win32 application regardless of the uninstall entry; only MSIX
packages uninstall from there. Keep InstallLocation as standard uninstall-entry
metadata and stop attributing Start menu behavior to it.
The concrete-terms gate forbids "provenance": the repository asks for the
exact source, field, or identity a value holds. The module records one
commit and whether its tree was dirty, so it and its callers say that.
The toolchain step also has to stay out of the way of the packaging
entry's error-recording test, which fakes the host platform and would
otherwise report the absent Windows compilers rather than the failure it
exercises. The test mocks the probe, as it already does for the release
environment and the signing keychain, and expects the new stage.
With a job registry composed, tool-bash and tool-pwsh now register every
command with ctx.jobs as it starts and wait on the job with the registry's
own bounded wait: a command that finishes in time returns the ordinary
foreground result and its record is removed, one that outlives the timeout
returns the same job id it already had, and a kill from outside the call is
reported as `[stopped: <reason>]`. Nothing changes hands at the deadline, so
the shell seam loses `onExpiry: 'offer'`, `ShellExecution.promotion`, and
`ShellPromotionOffer`, and both local executors drop their offer arm.
The registry stays optional: the tools register a foreground-only definition
while no registry is composed and swap to the job-backed one inside a
`ctx.inject(['jobs'])` fork, so a composition without jobs keeps a plain bash
with no job tools in the model's schema. A refused registration at the start
(admission, no controller) runs under the executor's deadline kill, and a wait
that expires before the process spawned ends as the preparation timeout.
JobRegistry gains `remove(id, caller?)` and the `settled` event reports
`awaited`, so dsh-tool-jobs no longer keeps its own wait ledger; it withholds
notices for awaited settlements and for kills the model requested. The tool
catalog harvest composes the registry so the shipped schema stays documented,
and the two compaction fixtures move their context window from 10000 to 10300
tokens because the branch's larger tool schemas pushed their condensed history
over the old trigger, which requested a fourth scripted model call.
Product dropped Harness-home cleanup from the uninstaller, so the opt-in page,
its bilingual dialog, the uninstall.ini home record and the overlap check go
away. The uninstaller now always removes the Electron user-data directory,
the %APPDATA% product directory and the updater cache through the native
helper; the Harness home is never touched. Silent uninstalls remove the same
data, upgrades retain it, and a locked file is logged instead of stopping the
uninstall.
Electron derives user data from the scoped package name, so the removal
targets %APPDATA%\@deepseek-ai\dsh-desktop and removes the empty scope
directory afterwards. The previous fixture used an unscoped name and never
reached that directory; the fixture now uses a scoped package name.
The native uninstall smoke runs end to end on the development host after two
test fixes: the interactive uninstaller was launched hidden, which made every
control invisible to the text search, and the Chinese finish text matched
"已经从" instead of MUI's "已从你". The native remover regression is renamed
data-cleanup-test.exe because UAC installer detection demanded elevation from
the unmanifested uninstall-data-test.exe.
Injecting a build version renamed the Electron shell but not the dsh
runtime packed into it, and afterPack required the two to be equal, so
every build carrying a build version failed after packaging finished.
The bundled runtime is the dsh package at the product version, which is
what the check now compares against; the app already reports the shell
version and the bundled runtime version separately to the update policy.
Rename the flag to --build-version. The repository already has
DOWNLOAD_TEST_RELEASE_ID for the opaque test feed directory, so calling
a version a release id invited a reader to confuse the two. Accept a
forwarded `--` as well, because `pnpm run <script> -- --build-version x`
passes the separator through as an argument and every following option
would otherwise parse as a second target.
Packaging a build for a test feed or a colleague required bumping the
version in 295 manifests plus the lockfile first, because the version a
build publishes was only ever read from the checkout. Those edits cannot
be committed, so every such build left the working tree dirty, and the
`<version>.<date>.<sequence>` convention behind them lived only in the
operator's memory.
The version a build publishes is now an input. `--release-id` names it,
`--release-id auto` numbers it after the builds already published to the
destination bucket (falling back to local artifacts, since a fresh CI
checkout has none), and it reaches electron-builder through
extraMetadata, the update feed, and the upload validation as one value.
The manifests keep the product version, aligned with the dsh npm
package, and a release still publishes exactly that.
Validation goes through `semver` itself, because `electron-updater`
compares feed versions with `semver.gt` against the installed
`app.getVersion()`: a build version must outrank the product version it
extends, which holds for `0.1.6-alpha.2.20260921.1` over
`0.1.6-alpha.2` and cannot hold on a stable version, where a fourth
release number is not a version at all.
Two things a build could not previously answer for itself:
- Artifacts now carry the commit they were packaged from and whether
that checkout was dirty, so a build handed to a colleague is traceable
without a tag.
- A production upload tags that commit as `desktop-v<version>` once the
artifacts are public, and reports what to run by hand rather than
failing an upload that already completed. Test and local builds are
deliberately left untagged.
A toolchain step now probes tar and the Windows installer compiler
before the first build step, so a host that cannot finish the sequence
says so in seconds rather than minutes.
The Windows uninstaller keeps every user file by default and adds a custom
page with unchecked options for desktop browser data and update caches, the
desktop plugin profile, and the complete Harness home. Selecting the whole
home requires a native warning that names the actual folder and the CLI
impact, defaults to Cancel, and forces the desktop child selection. Silent
and upgrade uninstalls never remove data, including through the legacy
--delete-app-data flag, which the adapted upstream template no longer honors.
Packaged Desktop records its resolved home in uninstall.ini under the
Electron user-data directory so a custom DSH_HOME is offered instead of the
default; a length field detects truncated NSIS reads and an invalid record
disables whole-home removal. The native remover in window-frame.dll rejects
protected roots and overlap with retained data, holds directory handles
during traversal, and unlinks reparse points without entering their targets.
Installation now also writes InstallLocation on the Windows uninstall entry,
which Explorer needs to associate the Start menu shortcut with this entry so
that the context-menu Uninstall launches the uninstaller directly instead of
opening the installed-apps list. The upstream template only records the
value under its private key.
Closes#3993
`tool-cordis` registered the four first-party Host inspect providers from
every preset row that mounted it. The Host registry rejects a duplicate
provider id, so a second preset carrying `tool-cordis` — or a user preset
copied from a shipped one — failed to mount in the same process.
- Add `@deepseek-ai/dsh-tool-cordis/host` (`cordis-inspect-providers`),
the host-plane row that registers the providers; the Web host
composition mounts it beside `cordis-host-runner`. The per-session
`tool-cordis` row now only registers the tools and its prompt section.
- Gate the `cordis` preset's `tool-plugin-manager` row on `profileContext`,
the same condition `dsh-base` applies to the host `plugin-manager`
service, so a profile-less embedding still mounts the preset.
spawnSync cannot start the pnpm .cmd shim without a shell, so the bump
step's lockfile refresh failed with ENOENT on Windows after rewriting
every manifest. Resolve pnpm to a JavaScript entry executed by the
current Node: the run-script's npm_execpath when present, otherwise the
workspace's own pnpm dependency (located on disk because pnpm's manifest
does not export its bin entry).
GNU tar parses the colon in an absolute Windows path as a remote-host
separator and aborts, so the tarball readers now run tar in the
tarball's directory and pass only its file name.
* refactor(preset): trim the creation-mode system prompt
The `cordis` preset persona repeated what the plugin_manager and
cordis_inspect_* tool descriptions and the two shipped skills already
carry. Keep only identity, the shipped-preset constraint, the visual
request routing rule, and which skill to load for what.
Drop the tool-cordis prompt section, which restated its own tool
descriptions, and the cordis_inspect_query navigation hints that the
provider manifests returned by cordis_inspect_list already state.
Shorten both skill descriptions so the first-turn catalog stays small.
First-turn request for the cordis preset, measured through the DeepSeek
API with deepseek-v4-flash: 9572 -> 8975 prompt tokens.
* docs: regenerate config catalog after section order removal
* docs: regenerate module graph after tool-cordis dependency removal
* refactor(preset): give the cordis preset the standard persona
Creation mode's system prompt now equals standard mode's; the skill
catalog and the shipped skills carry every routing and off-limits rule.
The plugin skill description states the visual-request destination.
First-turn request for the cordis preset: 9572 -> 8815 prompt tokens.
* fix(review): document current tool-cordis state and pin the shared persona
The tool-cordis README pair now states that only the two tool schemas
enter requests, that disposal removes tools and providers, and that
the first-turn skill catalog carries the routing. The Web skill row
test resolves the owner's selected fixture generation instead of a
fixed v3 path. The preset e2e asserts the cordis persona sections
equal standard's.
The 2026-07-16 explicit-turn-cancellation note still described a frozen,
detached signal reason and a two-variant cause union. The README paragraph
added here points readers at that note as the owner of the signal
lifecycle, so the stale mechanism was load-bearing.
It now records why freezing the caller's cause is not an alternative:
undici assigns `stack` from strict-mode code, so a frozen reason makes
fetch reject with `TypeError: Cannot add property stack` in place of the
abort reason.
Also names `turn/end`'s declared `TurnEndCancelCause` in the README and
takes the reviewer's helper-doc wording.
Re-render the four installer brand banners from the vector sources:
the badge follows the Figma spec (three-stop gradients, layered drop
shadows) and the whale no longer inherits shape-rendering=crispEdges,
which disabled anti-aliasing and left jagged edges. The wordmark now
reaches its design color instead of a soft #353638.
Draw the progress-page brand with high-quality bicubic interpolation
and half-pixel offset so the always-2x bitmap stays crisp when scaled
down at 96 DPI.
pi-ai 0.85.1 runs parseStreamingJson over the whole accumulated argument
JSON on every tool-call delta, so one large tool call costs O(n²) CPU on
the event loop and a multi-megabyte argument stream stalls every session
in the process. Patch the six stream adapters to parse once at
toolcall_end, document the patch, and cover both OpenAI protocols.
Fixes#4739
- main-startup: assert primaryRuntime for the unpackaged Host and cover a
missing DSH_DESKTOP_PRIMARY_RUNTIME_DIR as a fatal startup dialog with no
Host started.
- test-workspace-updates: pass the prepared primary-runtime directory instead
of an application/runtime path the runner never creates.
- development-project: derive the descriptor platform/arch from
resolveDesktopBuildTarget so it matches the prepared payload.
- main: resolve the unpackaged primary runtime once at startup instead of
inside the Host factory.
- dev: keep the environment override symmetric with its neighbours.
- README: document the variable as required for unpackaged launches and record
the remaining win32-arm64 payload architecture limitation.
The PTC card in Settings explained the mechanism (the agent chains tools
through code) rather than which tasks to pick it for, so readers could
not tell it apart from Standard mode the way the Minimal and Creator
cards allow. Reword the Chinese and English card and new-task menu copy
to name the tasks: batch tool calls whose results are then filtered,
organized, deduplicated, counted, or summarized. Update the four ARIA
snapshots that embed this text.
Node's fetch assigns an enumerable `stack` string onto the abort reason
object it receives, and the loop logged that same object into `turn/end`.
The undici trace became durable session data under a field no reader
declares. A transport adding a property JSON discards instead made
`Session.append` reject the ending, leaving a turn with no close.
`abortedCancelCause()` narrows the untyped `AbortSignal.reason` once and
copies the declared fields, replacing the three `as AgentCancelCause`
assertions at the two read sites and the log write.
* feat(cordis): add volatile config references without remounting
* refactor(cordis): use internal prefix for volatile updates
* fix(schemastery): preserve metadata-free serialized schemas
* refactor(config): move volatile updates into an opt-in Loader plugin
* refactor(config): reuse Loader context patch for volatile updates
* refactor(config): let the volatile watcher commit equal candidates
* refactor(config): separate comparison from volatile reconciliation
* refactor(loader): extract pure entry option diff
* refactor(loader): compare volatile config through schema metadata
* refactor(loader): make volatile comparison policy explicit
* refactor: name config reload and volatile HMR events
* refactor(loader): compare volatile config in one schema walk; declare hmr/volatile-update in volatile-hmr
Replace the mask/project pair in vendor/loader/src/config/diff.ts with one recursive schema-guided comparison. Absent objects now compare as their declared schema default at every object node, not only under volatile ancestors; both inputs resolve to the same validated config, so the one changed assertion (undefined vs {} under a non-volatile object schema) becomes unchanged. The isJsExpr special case is dropped: expression keys fall through to raw comparison.
Move the hmr/volatile-update Events declaration from vendored Cordis core to its producer, packages/boot/volatile-hmr, matching how dsh-hmr declares hmr/change and hmr/reload. Vendored cordis events.ts returns to upstream content; the inherited Cordis API catalog and the event matrix are regenerated, and the tutorial example imports the package types.
* fix(cosmokit): compare sparse array slots in deepEqual; extend volatile diff coverage
Array.prototype.every skipped holes, so deepEqual(Array(1), ['new']) was true in one direction. Compare every index with holes read as undefined.
config/diff.ts treats expression objects as opaque values even when a schema field shares the marker name, and looks up field schemas with Object.hasOwn so unknown keys named after Object prototype members compare raw. Tests cover absent-object defaults, sparse arrays in both directions, prototype-named keys, metadata-free serialized schema nodes, and a runtime remount check in volatile-hmr. READMEs and the architecture note describe the object-default rule.
* test(schemastery): build serialized schema fixtures without unknown casts
verify-no-unknown-casts rejects the two as-unknown assertions in scripts/volatile-config.spec.ts. Parse the serialized nodes from JSON text instead, matching scripts/loader-config-diff.spec.ts.
* docs(volatile-hmr): drop the scope-scan tag from hmr/volatile-update
verify-scoped-events rejects @dshScopeScan on an event that carries no scoped keys. Catalogs and the event matrix are regenerated.
* docs: regenerate module graph for the volatile-hmr dependency edge
verify-module-graph reports the merged docs stale: volatile-hmr now lists its app-boot dependency.
* test(volatile-hmr): watch the real YAML through an exact config watch
The Windows coverage lane lost the whole spec: its vitest worker exited without a report once this test started a chokidar directory watch (root: ['.']), the only real directory watch in the test suite. Register the include file through hmr.watchConfig instead, the pattern the HMR package's own real-file tests run on Windows, and emit loader/config-reload from the refresh callback as the root-watch path does. packages/boot/hmr/tests/coordination.spec.ts already covers that root-watch emission with a mocked watcher; volatile-hmr and hmr sources stay at 100% coverage.
* test(volatile-hmr): pass the Include path as a file URL
On Windows the plain absolute path made the include subtree fail to load, so the profile-patch test found no consumer entry. Other Include fixtures in this spec already pass file URLs.
* refactor(loader): commit volatile config in Entry.update and remove volatile-hmr
loader/config-reload was emitted from two places (app-boot profile reconciliation and the HMR root watch) and by hand in tests, because a separate plugin needed a "configuration applied" signal to run its own pass. Every update path already funnels through Entry.update, which is where the Loader decides to skip a remount for a volatile-only change. Entry._commitVolatile now parses the candidate through the fiber's internal/config hook and schema, commits the new values into the running references when the ordinary effective values still match, and emits loader/volatile-update to the owning fiber. A changed ordinary effective value falls back to the ordinary remount instead of leaving the instance stale; an invalid candidate is logged and keeps the running references, with the raw config retained for the next activation. Forced group updates take the same path.
Remove packages/boot/volatile-hmr, its event, the app-boot loader/config-reload declaration and emit, and the HMR emission. Loader runtime and real-file tests move to scripts/loader-volatile-update.spec.ts. Docs, catalogs, the event matrix, module graph and the architecture note follow.
* test: share the volatile schema fixture between the schema and Loader specs
The duplication gate flagged the identical Config fixture in scripts/volatile-config.spec.ts and scripts/loader-volatile-update.spec.ts.
* refactor(loader): reduce config/diff.ts to equalExceptVolatile
Entry.update now computes the strict raw option changes itself and only asks diff.ts whether two raw configs differ at most in volatile fields. The ignoreVolatile flag, the options object and the "config is the sole change" rule leave diff.ts; they were the Loader's decision and now sit next to the commit they gate. The raw-config deepEqual in the volatile branch is gone because the strict change list already answers it.
* refactor(loader): type the volatile comparison against Schemastery's Schema
config/diff.ts narrows the plugin's Standard Schema to Schemastery's own Schema type instead of a hand-written structural interface, which makes vendor/loader a type-only dependent of @deepseek-ai/schemastery. The rescope codemod treats the vendor identifier comparison like schemastery's own source and renames the import through an exact edit.
* fix(loader): address review findings on the volatile commit path
Entry.update only replaces the fiber's pending raw config for a volatile-only change, so a noSave direct update survives a no-op entry update. _commitVolatile logs at debug level when ordinary effective values changed and the ordinary update applies, dispatches loader/volatile-update with a Context receiver like Cordis' own filtered emits, and isolates a throwing listener. Schemastery reports unsupported volatile values as ValidationError with their path and validates a lazy inner once when it is built. The inherited-event catalog gains internal/config and corrected source lines; the Loader spec imports dsh-hmr by package name and covers the new behaviors. Docs record that class-instance ordinary fields always take the remount path and that .default() accepts the raw input type.
Review follow-ups on the refused-creation notice:
- `openWorkspace` raises `createFailed` only while its navigation is
current, so a refusal that lands after a later navigation or owner
disposal stays silent — the gate `initializeDefaultWorkspace` already
uses and what the README states; two service cases pin it.
- `RowActionToast` switches on the closed notice union and ends in
`assertNever`, so an unhandled kind fails to compile instead of
rendering another notice's copy.
- `apps/web/tests/new-session-refused.e2e.ts` drives the composed Web
server with a user preset whose row cannot resolve and compares the
notice's ARIA golden, beside the default-Workspace failure scenario.
- `@throws` on `openWorkspace` covers every failure (catalog
regenerated); `RowToast.createFailed` records why the Host code stays
in the copy; the README states that other failures show their own
message.
Numeric download progress renders without an ellipsis, and a failure keeps the update chip's palette instead of a separate error color. Pin the progress label and the shared badge background, and let the retry label carry the failure distinction.
An explicit New Session (sidebar and header buttons, the workspace row
button, and the hero workspace pick) whose `session.create` the Host
refuses — for example a default preset whose composition names a
plugin that no longer resolves (`agent-preset/invalid`) — was only
logged with `console.warn`, so the click looked like it did nothing.
`openWorkspace` now shows the failure through the shared Workspace
notice as a `createFailed` toast quoting the Host's stable code and
message on the long hold, then rethrows as before. Startup restoration
does not go through `openWorkspace` and stays console-only.
CSS animations start when their element is inserted, so loaders mounted
at different moments rotated out of step in the sidebar session list.
The ongoing loader now pins its spin and dash animations to document
time zero on mount so every visible loader shares one phase.
The macOS window's draggable area was a hand-written band: a 52px strip, deepened
to 76px by a marker the Conversation header had to keep in step with its own
height. It drifted from the layout — blank runs inside a chrome row did not drag,
and a control a few pixels away dragged the window instead.
Chrome rows now mark their own element (`data-window-drag`) and the shell declares
the surface once (`html[data-platform='darwin'] [data-window-drag]`), so a row's own
box is its draggable geometry and no fixed band has to match any row's height. The
frame band, its 52/76 figures, the marker, and the per-sheet drag rules are gone;
interactive controls and overlays portal beside `#root` where one rule subtracts
them.
Recollection becomes one shell watcher (`window-drag/recall.ts`): Electron rebuilds
the window's drag rects only when a computed app-region value changes
(electron#32341), so the watcher measures every marked row each frame while the
surface moves, sets `data-window-drag-recall` for the frames whose geometry changed,
and clears it on the frame that finds the geometry settled. The right panel's
hand-written pulse is deleted with it.
Coverage is a contract with executable parts. `window-drag/regions.ts` publishes the
composition rule and the interactive selector that both the sheet and its spec
compare against; the ui-theme gate asserts the package tree declares drag in exactly
two places and pairs every row's mark with its sheet, selector, and authored height;
and the browser lane boots the shipped composition in Chromium with the darwin
marker to assert per-row geometry, a content body under a marked head row, a
covering overlay, and the watcher's pulse. The plugin manager's detail views mark
the head row they share rather than the pane they render in, so their text and form
labels stay content.
Both real machines agree: the macOS matrix ran on a real `hiddenInset` window with
HID-level pointer input and window-frame readback, and the Windows matrix on a
Windows 11 ARM64 VM — chrome rows drag, content and the covering overlay do not,
and the controls keep their clicks.
Refs #4619
Every ring that used 2px now reads --dsw-focus-ring-width. The dense tables and
toolbars keep their 1px ring on purpose: at 2px they visibly outweigh the rows
they sit between, so the standard value and the dense exception stay distinct
rather than converging on a number nobody measured.
The action cluster animates its width with overflow hidden, and the search pill
clips to its rounded shape, so an outer ring on the add, search and clear
controls was cut to a sliver. This package declared no focus style at all — every
control fell back to the browser default inside those boxes.
Ring the three controls inside the element, the direction issue #4655 allows.
* feat(bundle): accept an ordered patch file list in dsh.bundle.patch
`dsh.bundle.patch` now accepts `string | string[]`. A list names patch
files applied in order as one bundle layer; each file's relative plugin
paths still resolve beside that file. `bundlePatchPaths` in dsh-app-boot
is the one resolver from a declaration to absolute paths, and every
reader uses it: profile loading, plugin-manager reconcile and bundle
rows, the client-runtime roster, and the workspace, cordis-config, and
product-isolation gates. `ProfileLayer.patchPath` becomes `patchPaths`.
* fix(bundle): keep declared patch files package-relative in the workspace gate
path.join produced backslash entries on Windows, so the expected files
list did not match package.json files. bundlePatchFiles returns the
validated declaration as written; bundlePatchPaths joins it.
The shell registers two General rows after #4716, but README.md and
README.zh.md still described only Developer tools in three places each.
Align both sides and re-record the bilingual pairing hashes.
Also close the review nits: assert the settings.general.item rows dispose
on unload, give the localized-label cases distinct titles (%s printed the
dictionary object, so both read "[object Object]"), complete the
CurrentVersionRow @param, and note why current-version orders last.
The Desktop preload reports a picked file's real path through
__DSH_HOST_PATHS__. Where that bridge exists, the composer inserts a dropped,
picked, or pasted folder or non-image file as the same @path reference chip
the user could have typed, so nothing uploads and the model reads the target
with its file tools; images keep uploading. The drop handler names dropped
directories through the entry API, and a browser without the bridge refuses
a dropped folder with product copy instead of failing it as an upload.
Tests bring the changed sources to full per-file coverage, the READMEs and
docs describe the uplink, the invocation context, and the Peer registry in
both languages, and the generated catalogs are refreshed.
Every Typert Remote stream accepts client-to-Host items on the same
logical stream. A method declares the uplink item type in its return
type, RemoteStream<Out, In>, and reads the items through
this.ctx.invocation.uplink(); the invocation also carries the calling
Peer. Client generated methods return a handle that iterates the
downlink and exposes send/end/dispose for the uplink.
The composer's plan-mode chip rendered its label followed by a 12px
one-pixel cross, out of step with the sibling access-mode trigger's
glyph-then-label layout. The chip now leads with the 14px plan glyph
(the /plan command's own icon) and swaps it for a new circled close —
IconCloseCircleFill, a filled disc with the cross knocked out — while
the enabled chip is hovered or keyboard-focused, so the pending click
reads as turning plan mode off.
The pill's corners move from a 999px circular capsule to the compact
Button geometry (14px on the 28px row) under the theme's superellipse
curvature, matching every neighbouring control; the lifecycle-chrome
probe now pins that radius and the shared corner-shape.
* fix(web): bind the right Sidebar before same-commit mount effects
A plan review arriving while a global panel was active crashed its opener when the user returned: the frame mounts the Conversation column ahead of the right Sidebar seat, so PlanReviewOpen's mount effect called ctx.sidebarRight.openResource before the seat's passive bindService effect had published the binding. The slot runtime retired the crashed entry for the rest of the page, removing the View full plan link and every later automatic open.
Publish the seat binding from a layout effect, which runs before any passive effect of the same commit regardless of tree order. The seat spec mounts a Conversation-column opener ahead of the seat and returns from a global panel; the plan-review Web scenario delivers a review while the Plugins panel is open and expects the preview and the link after the return. Both fail before the fix.
* fix(web): open the plan review once the Sidebar seat is mounted
Replace the layout-effect binding with an observable: ctx.sidebarRight.mounted publishes the session whose seat is on screen, undefined while a global panel replaces the Conversation. The seat keeps binding from its passive effect. PlanReviewOpen reads the source through a bound hook and opens once a seat is defined, so a review mounting ahead of the seat in the same commit no longer assumes a bound seat and never crashes its entry.
The seat spec's opener stand-in now reads `mounted` the way a slot component does; the service spec pins that only real transitions publish, and the PlanReviewOpen spec covers a review that mounts before any seat and opens once the seat binds.
* test(web): align the plan-review console tripwires and the seat term
The off-screen review case now asserts the reconnect warnings like its siblings and filters crash logs by error level as the other e2e files do. The zh README keeps 席位 for the seat throughout.
* test(plan): keep the review props cast at its baseline fingerprint
verify-no-unknown-casts counts a modified assertion as new. The two PlanReviewOpen specs now share the unchanged baseline cast through one helper and supply the seat hook as a typed value beside it.
A tooltip asks what the last input was: after any key the user is working from
the keyboard, so a programmatic focus may raise a bubble. A focus ring asks
something narrower — Chromium reveals an already-focused control as soon as any
key arrives, including a modifier or Escape that never navigated anywhere, which
is the ring users report as unexplained.
Record both answers and publish the ring's narrower one, so tooltips keep their
existing behaviour and rings stay silent for a pointer's focus.
The breadcrumb trail and the Miller column scroll, and the column's left edge
carries no padding, so an outer ring on their buttons was clipped to a sliver.
The TurnNavigator marker sits at the scroller's right edge, which clipped three
of its four sides. Ring those inside the element instead — the direction issue
#4655 allows — and change no layout value.
Clicking a control leaves it focused with :focus-visible false, so nothing is
drawn. Chromium then reveals the ring the moment any key arrives — a modifier,
Escape, a typed character — which is the unexplained focus ring users report:
they never navigated there, yet a ring appears under the pointer's last target.
Record the input modality on the document element, publish the keyboard modality
only for focus navigation (Tab, arrow keys), and let the global focus sheet keep
a pointer's focus transparent. Tooltip's pointer modality moves into the same
module so tooltips and rings follow one rule.
The shared rule now states why the collapsed identity and the expanded value keep two label tiers, and the detailName comment names the row identity it also renders.
Focus rings named eight different colours across 54 client stylesheets, and a
control that declared no ring fell back to the browser default, whose colour
follows the operating-system accent — so the same build showed orange on one
machine and blue on another.
Every keyboard focus ring now uses --dsw-alias-brand-primary. A global ui-theme
focus.css sheet reaches the controls that declare no ring of their own, and 49
declarations name the same token. Only the colour changed: each declaration
keeps its width, style and offset.
The occupying badge chooses its own side, and the update badge keeps a right-opening bubble that the Windows caption menu can cover; ui-settings-general records that as a known limitation.
Web and Desktop now share one command pair: start:* launches the artifacts
of a prior complete build and dev:* builds first, then launches. start:web
is new and spells out the same source launch vector as `pnpm dsh web`;
dev:web runs `pnpm run build`, the three rebuild watchers, and `dsh web` in
one terminal, with --skip-build and --no-serve for the previous two-terminal
flow. The launcher gate pins both scripts to the dsh CLI, and a root
Makefile names the same commands as make web|dev-web|desktop|dev-desktop|build.
The dev-web stage supervisor waits for stages the terminal already
interrupted, escalates only survivors, forwards SIGTERM once, and reports a
stage that exits on its own. Signal listeners are persistent because the
tsdown watchers bundle signal-exit, which re-raises a signal when it finds
no other listener.
The previous sentence claimed every caption icon bubble opens below the caption, but only the sidebar toggle and New Session derive side=bottom; an occupying sidebar.toggle.badge keeps the default side.
The unpackaged shell derived the primary-runtime directory from process.arch, while preparePrimaryRuntime() resolves the target through resolveDesktopBuildTarget(), which fixes Windows to x64. On a Windows ARM64 host the shell looked under targets/win-arm64 while the payload was under targets/win-x64, and the missing office-skills/scripts/check_office.py became a fatal startup dialog.
The launcher now resolves that directory through the same resolver and passes it to the shell as DSH_DESKTOP_PRIMARY_RUNTIME_DIR.
Address review: retention now has a single definition (retainedIds) shared by
the list-refresh sweep and the single-id prune, an already-engaged identity
skips the redundant row mutation, and the ledger documents why Host-asserted
running is recorded before any holder exists while the client-local acceptance
callback requires one.
@yao-pkg/pkg's SEA bootstrap extracts native addons from the VFS into a
per-user cache ($PKG_NATIVE_CACHE_PATH/pkg/<sha256>/) with plain
writeFileSync, so concurrent first starts of the same executable truncate
files another process has already mapped: SIGBUS on Linux, "Library not
loaded" for sharp's libvips on macOS. Extend the existing pnpm patch so the
three write sites go through a same-directory temporary file and
renameSync; a cache path is then either absent or complete, every process
finishes its own copy pass before it loads, and identical content makes
concurrent renames harmless without a lock. Cache location, hash keying,
and per-file comparison are unchanged. The single-exe Agent Note records
the decision and the measured before/after runs.
Closes#4664
The no-unknown-casts ratchet landed on master after this branch, so the new stub tables in the ACL failure paths can no longer assert through unknown. Route them through a typed partial stub, type the bigint handles as NativePtr, and prune the three retired baseline entries.
The Client Slot catalog serves `cordis-client-runner` closures, which cannot
`require` anything, so the `sidebar.workspaces.session.menu.item` example
now renders a `role="menuitem"` `<button>` and the doc names
`MenuItemButton` as what the shipped rows use rather than what an entry must
render; the README separates the Module Loader lane, which does reach the
primitive through `require`, from the runner lane. The Agent Note and the
contract module doc say the entries read the pin and archive sets through
hooks their own faces inject, that the owner draws no separator, and that
the strip keeps clicks from opening the row. `RowActionToast` gets its
contract sentence and the rename-assembly spec points at the e2e that
exists.
The Session row's action strip stops click propagation itself, so a
`sidebar.workspaces.session.row.action` entry that renders a plain button no
longer opens the row as a side effect; the shipped buttons drop their own
handling. `Menu` decides submenu exclusivity on the list's own bubbles, by
DOM: reaching a top-level row that is not a submenu parent, by pointer or by
focus, closes the open card for data rows and component rows alike, which
the per-row handlers only did for data rows. A group start on the list's
first row draws no hairline.
The first live-fixture e2e case registers the scaffold's cleanup right after
launch, as the file's other cases do. The three pin scenarios the browser
spec lost when the order write moved into `UiWorkspace.pinSession` — newer
saved orders during a pending pin, a Workspace disappearing during one, and
members whose summaries are temporarily missing — return as service cases.
Re-record the session-snapshot README bilingual pairing sidecar, and admit
the source-reference regression fixture with the v4 producer-owned `user`
message source kind.
The fixture plugin's rows join `sidebar.workspaces.session.menu.item` after
the shipped rows, the first one opening the plugin group with
`separatorBefore`, and dismiss the menu through `useMenuOpenState`.
The typert Cordis catalog drops any exported name that two declarations
share, so ui-workspace's `SessionRenameRequest` made the generator remove
Session Controller's request type from the model-facing API catalog. The
row-side pending rename is `SessionRenameTarget`: the Session and the title
the dialog seeds from.
A Session row's "..." menu and its hover buttons become two list slots the
WorkspaceBrowser entry declares, `sidebar.workspaces.session.menu.item` and
`sidebar.workspaces.session.row.action`. The shipped actions register from
apply the way a client plugin does: pin (menu 100, button 200), rename (200),
fork (300), archive (menu 400, button 100). An entry receives only the row
identity, reads the pin and archive sets through Set hooks its own face
injects, decides its own visibility, renders `MenuItemButton` or one icon
button, and dismisses the menu through the slot-level `useMenuOpenState`
hook bound from the row's render occurrence.
Each action's behavior lives in its injected callbacks. `UiWorkspace` gains
`pinSession` and `unpinSession`; the pin write into the saved orders happens
there, through the viewing-store instance the apply creates and hands the
browser's registration as its handle (the ui-layout pattern), reading the
memberships current at completion. The rename dialog and the row-action
notice are `shell.overlay` entries fed by apply-closure observables through
the faces' `hooks`; the archived notice switches the archived filter
directly. WorkspaceBrowser threads no action callbacks, renders no toast,
and hosts no rename dialog; its title double-click raises the same rename
request.
In ui-primitives, `MenuItemButton` is the plain `role="menuitem"` row for
menus whose rows are components: it shares a data row's markup and styling
and needs no shared state, because `Menu` decides the keyboard walk from the
DOM and now runs the post-selection focus return once on the list's click
bubble for every row kind. `Menu` accepts such rows as `children` beside
`items`, which becomes optional together with `onSelect`; `separatorBefore`
lets a row start a group with the same hairline a separator entry draws. The
context-based `MenuAction` goes away with its extension group.
Review feedback on this PR: keep one identity colour rather than raising the
card subtitle to label-secondary. The detail-page package name moves from
label-caption to label-tertiary; the settings card subtitle keeps its own
colour and shares only the 12px size with the row it expands into.
master's session row menu now leads with Pin session and SessionNode
carries pinned/archived, so the slot-action specs and the Web golden take
the row as it is after the rebase.
The cold row's updatedAt is the later of the header's creation time and the cached last prompt; the fork's creation time is wall-clock and can trail the inherited prompt by a millisecond, so the restart test asserts the same maximum instead of the prompt time alone.
A real fork made through sessions.fork is checkpointed into the projection cache, the Context is disposed, and a Host booted over the same storage root serves the fork's title and list metadata from the header alone, as a kind: 'cached' block, without inspecting or opening the log.
The session-controller README, the per-session store JSDoc, and the projcache predecessor-recovery and durable-web-queue-recovery notes described one higher-seq-wins rule and the asOfSeq -1 title hint. They now state that higher-seq-wins applies among Host-sequenced values, that cached list blocks yield to the connected Session's baseline, and that the predecessor title carries its stored row's watermark under kind: 'cached'.
The built-client fixture served newly created Sessions with a control-baseline block as their list projections. Since SessionProjectionHints requires `kind`, the real client's applyListBlock would have hit assertNever on it; the created Session is live on the fixture Host, so its block is sequenced.
The listing identity note describes the `kind` field, rejects routing on the -1 sentinel, and links the projcache predecessor-recovery and projection-owned client state notes; the latter's client merge rules now distinguish Host-sequenced rows from cached list hints.
A `sequenced` list block merges key by key under higher-seq-wins like any Host-computed value, so a delayed control baseline at a lower cut can neither overwrite nor clear a newer live-session list value. Only a `cached` block goes through applyCached and yields to every sequenced write. The projection store's module docs describe the two row kinds.
SessionProjectionHints gains a required `kind`: `sequenced` when the Host's live registry computed the block for an attached Session, `cached` when a header-only listing viewed it from the persisted projection cache. `asOfSeq` stays the producer's own watermark in that space, so the cache read faces report the lowest served row seq again instead of the -1 sentinel. The two fields are independent: `kind` names the sequence space, `asOfSeq` the position in it.
Agent Note for the lifecycle identity read-only face and the client store's cached/sequenced row kinds: mechanism, rejected alternatives, and what stays untouched.
ProjectionValueStore rows are either cached (viewed from the persisted checkpoint by the session list, no comparable seq) or sequenced (computed by the connected Host, seq comparable within the connection). Cached values fill empty keys only; every baseline discards cached rows before seeding, and every frame replaces them regardless of seq, so a connected Session's values always supersede list hints. The list response and api-session/added blocks write cached rows.
The projection cache read-only face (cachedSnapshot, cachedPredecessorTitle) now matches the lifecycle identity a header witnesses (formatVersion, createdAt, cwd, isSeeded) and serves its rows as a cached block with asOfSeq -1. The fold face (hydratePrepared, write, coldSnapshot) keeps the full identity including the inherited cut. The session list and @ completion drop their isSeeded guards, so forked sessions keep their cached title and list metadata across a Host restart; list-children keeps its body fallback.
The package name under a plugin's title is the exact spec that installs it, yet
it was the palest alias in the palette: label-caption, 2.13:1 against white,
lighter than the free-text description below it. Use label-secondary, the same
alias the description uses, for 5.80:1.
The profile resolution lookup order note, merged today, owns the mechanism this
note describes and names it runtime resolution with an interception layer at
$DSH_HOME/profiles/node_modules. This note adopts that vocabulary and links the
owning record instead of restating the resolution order.
It also records the boundary the sibling experimental providers leave open: a
profile that installs one of them beside this package hoists their
dsh-mcp-client dependency into the profile's own node_modules, so this package's
peer resolves there and the two copies keep separate serverName reservations.
The scope split does not return, because none of them declares dsh-scope.
Issue #4628 tracks moving those declarations to peers.
Refs #4628
Keep the Loader entry id at one size in both places it appears - the card
subtitle and the row the card expands into - by moving the shared code-font
typography into a single rule. The subtitle keeps its own secondary label
colour against the expanded row's primary one.
Interpolating an empty body directly produces empty parentheses in the failure title, making the empty-response case harder to distinguish from the truncated JSON case in CI logs.
Use JSON formatting for the existing case object so the operation and body are visible, including an explicit empty string. Keep the same inputs, error classification, original-cause assertions, and single-request checks.
The attachment-policy fixtures introduced three assertions to unknown and
changed two previously recorded assertions. The new repository gate rejects
those escape hatches even though the earlier branch CI passed.
Give the user, tool-result, and inbox fixtures their required V4 message
metadata and declared identifier types. Declare the fixture's plugin content
tag through ContentBlockMap, and check the flat tool-result fixture directly
as its concrete SessionEvent type. Keep intentional malformed inbox entries
and opaque extension fields in the existing negative cases.
Remove all five unknown bridges without adding allowances. Delete the two
baseline entries retired by the rewritten existing fixtures; the other
three assertions were never part of the accepted baseline. Attachment
runtime code and the tests' access-denial and complete-log assertions remain
unchanged.
Validation: the Host TypeScript aggregate and all 52 archive tests pass.
verify-no-unknown-casts reports no new or stale entries, with 1575 existing
assertions remaining; the baseline adds zero allowances and retires two.
The operation, HTTP status, and parser cause added by responseJson belong to JSON decoding failures. Existing metadata validation also uses INVALID_RESPONSE but does not provide those fields, so the broader phrase parsing failures promises too much.
Limit the README guarantee to JSON decoding and keep metadata-validation behavior unchanged. Keep the Agent Note focused on why the existing INVALID_RESPONSE code is used without inferring a transport retry, and link to the README for the error-field details. Update both languages and their pairing records.
Attachment reads and ZIP export previously inferred ownership from common
field names on arbitrary events. Opaque ignorable plugin payloads could
therefore cause storage reads, while declared compaction content was missed.
Select attachment-bearing fields by current built-in event type in both
readers. Read direct image and file blocks, including flat V4 tool results
and developer messages, without interpreting extra block fields. Leave
unknown payloads and malformed inbox entries opaque. The archive preserves
the complete logical log even when its payload does not authorize a read.
V4 validates title requests as one text block, so do not treat them as an
attachment carrier. Compaction keeps its declared ContentBlock fields for
valid custom producers; the basic producer's text-only output is documented.
Historical wrappers remain the adjacent converters' responsibility rather
than a second compatibility path in attachment readers.
Adapt the original policy change to master after V4 landed, retaining its
author credit and scope. Cover tool-result export explicitly, keep Team
fixtures free of invented message roles, and verify that denied carriers
never read storage. The 889 owning tests pass with 100% statement, branch,
function, and line coverage for both changed runtime files.
Co-authored-by: Tianyi Cui <[email protected]>
The new no-unknown-casts check rejects two assertions introduced by these projection regressions. Both fixtures intentionally represent unknown content: malformed input sent to the admission checks, and a cached plugin block whose opaque field was already lost.
Declare those inputs as unknown variables instead of asserting their expressions to unknown. Keep the same malformed values, content assertions, checkpoint replay, and production validation. Do not expand the recorded cast allowances or change any runtime behavior.
The cold checkpoint regression calls only the projection registry register and restore methods. Neither needs SessionStore, so mounting and disposing that extra service obscured the fixture dependency. Keep the registry lifecycle and the recovered-content assertion unchanged.
The Team projector declares only a subset of current content validators; the existing file and image validator gaps are not fixed by opaque JSON preservation. Limit the new prose to those local validators instead of promising full ContentBlockMap validation.
Keep cache behavior in the README and invalidation rationale in the existing decision note. Document why unknown JSON objects pass through by reference without changing any parser branch or expanding this repair into unrelated validation policy.
A valid unknown content block can own a __proto__ JSON field. Zod object reconstruction silently drops that key when Team events are projected and when checkpoints are admitted, even though the original Session log still contains it.
Validate the unknown block tag without reconstructing its decoded JSON fields. Keep the current V4 local validators and the rejection of retired tool-result content. Bump only the Team projection cache stateVersion from 3 to 4 so old lossy checkpoints are rebuilt from the unchanged log; this is not a Session format or schema change.
Cover direct and nested opaque JSON fields inside accepted native V4 content, plus public registry restoration from an older lossy checkpoint. Historical tool-result conversion remains owned by the Session migration and is not restored in the Team parser. Update the README and existing Team decision rationale together.
Adapted from the Team JSON preservation work in b8f1da92599123e5d5bae6c174f73b5b778f3554 by Tianyi Cui.
Co-authored-by: Tianyi Cui <[email protected]>
Empty or truncated successful Files API bodies currently escape as bare SyntaxError values. They omit which Files operation failed and the successful HTTP status, obscuring the diagnostic while the caller still needs to reject the response.
Route upload, list, retrieve, and delete JSON parsing through one helper that converts only SyntaxError to LlmError INVALID_RESPONSE. Preserve the operation, HTTP status, and original parser cause; transport and cancellation failures retain their original identity. Do not introduce retries or reinterpret malformed JSON as a successful response.
Keep regressions for both Messages and Chat Completions, all four operations, empty/truncated bodies, single request execution, and non-parser error identity. This improves classification and diagnostics without claiming to repair the remote cause of intermittent empty responses.
Co-authored-by: Tianyi Cui <[email protected]>
The merged concurrent migration test used an as-unknown widening assertion after JSON.parse. Master now rejects newly introduced unknown casts, so that test blocks the repository gate and unrelated PR validation. Give the map callback an explicit unknown return type instead. Keep the gate baseline unchanged; parsing and assertions have the same runtime behavior.
Name the JSONL backend and explain that its cache reuses recently decoded logs. This makes the bounded-batch rationale concrete without duplicating private implementation constant names in the operational description.
An unexpected visitor rejection represents a batch-level programming or reporting failure, rather than a normal per-Session refusal. The scheduler already drains its active workers before surfacing it and does not start deferred retries. State that behavior explicitly so callers do not assume the retry pass runs after a fatal batch failure.
The parent-child integration case can succeed without observing a race, while scheduler-only tests do not exercise the actual error classification or CLI output. Inject one genuine source-change error at the persistence open boundary in an isolated child process, then delegate to the real backend. Assert DEFERRED, completion of the other input, one RETRY, preserved source bytes and the resulting parent catalog. This adds no production test hook or timing-based race.
The command promises any positive safe integer, but its additional decimal-spelling regex rejected valid expert overrides such as 1e2 and +1. Let Number parse the supplied value and retain the positive safe-integer check. This preserves the CPU-based default cap while allowing equivalent numeric spellings and larger explicit values.
The source-entry regression creates a Windows junction to the real checkout. Its local unlink normally removes it, but fallback teardown must remain safe if that unlink fails. Use the repository fixture cleanup helper to unlink reparse points before recursive deletion and retain its bounded Windows handle-release retries. This changes only test resource cleanup.
A text error list does not show how failures are distributed across historical source versions or let developers aggregate repeated rejection reasons. Emit and save the same JSON report with per-version outcomes, stable failure groups and per-input diagnostics, plus runtime, checkout and report locations. Keep source versions from physical discovery rather than migrated handle headers.
Classify only known diagnostic fields such as unexpected members, unknown events, child prerequisites and sequence gaps. Preserve original messages and use an explicit fallback for other failures; the report makes no inference about historical producer bugs and cannot influence admission or retry decisions. Regression checks cover printed/file/log equality, private report files, grouped distinct sequence coordinates and source-file preservation.
Review follow-up: nothing consumed the field. The reserve stays an input to
`resolveCompactSpec`, whose returned budgets already express its effect, so
`ResolvedCompactSpec` keeps its exported shape.
Note the name collision a consumer would have hit: `ResolvedTargetPolicy.maxTokens`
is the compaction *summarization* output cap (default 8192), not the routed
request's cap reserved from the same window.
Proactive pressure scaled `thresholdRatio` by the routed model's entire
context window, but a request also reserves output tokens the provider
charges to that same window — the LLM service deliberately never clamps
them (`packages/llm/llm/src/index.ts`).
With a 1,000,000-token window and the default 256,000-token request cap the
gate therefore sat at 800,000 while the provider refused any prompt above
792,576. Observed in a real session: three requests died at 792,850 /
797,639 / 793,352 message tokens ("maximum context length is 1048576 ...
you requested 1048850 tokens"), no pressure compaction ever fired, and the
overflow recovery that did run could not condense an ~800k-token history
into the 8192-token summary cap.
Scale both ratios by the message budget left after the reserve, sourced from
the effective request envelope's `maxTokens` and falling back to the
adapter's per-request output default. A reserve that leaves no message
budget reports a target-specific pressure configuration error, matching the
existing per-route warning policy rather than silently disabling proactive
compaction. The headless compaction e2e advertises a coherent capacity/cap
pair so its 8000-token message budget and 4000-token threshold are unchanged.
Related #1841
Sequential corpus migration waits for each Session to finish I/O and publication verification before starting the next. Use a bounded queue with one shared persistence Context, defaulting to available CPU count capped at 16. Expert --jobs overrides accept any positive safe integer. Stable input indices and separate completion counts keep interleaved progress useful for large corpora without opening every log simultaneously.
A concurrently upgraded child can invalidate its parent evidence. Preserve the backend revision checks and defer only the explicit source-change error for one serial retry after initial work drains. Corruption, unsupported formats and writer locks remain failures; historical files and existing V4 data remain unchanged. Deterministic scheduler tests cover 10,000 inputs, bounded overlap and retry ordering, while real raw/zstd corpora verify parent catalogs and serial-equivalent output. Performance timing was stopped at user request because the host has competing workloads.
Give developers one pnpm command to exercise the release migration against their own Session roots. Inventory physical generations so malformed entries remain visible, then reuse JSONL write-open locking, validation, and exclusive successor publication with the full root available for child-catalog evidence. Open existing V4 files read-only and preserve every historical generation.
Report each selected source and outcome, continue after individual failures, repeat all errors at the end, and retain a private temporary report with checkout/runtime details and full causes. A failed run exits nonzero. Explicitly close handles because source-launch await-using lowering inside a loop can retain a previous iteration error. Real-entry regressions cover raw/zstd tool-result conversion, repeated runs, unchanged originals, failure continuation, and unsupported layouts. Document the command in the migration cookbook.
The V4 acknowledgement still said missing child descriptors reject migration. The converter already skips backfill when a child has no single supported own descriptor, while explicit identity, timestamp, and mode conflicts still reject publication. Align both language versions with that behavior so release reviewers and corpus testers do not expect an obsolete refusal. Preserve every machine acknowledgement and schema fingerprint.
The conversation fixture used an undeclared goal source and forced it
through MessageSource with a double assertion. That bypassed the V4
producer-owned source types instead of exercising an input accepted by
createUserMessage.
Use the fixture's existing declared test source for both injected plugin
messages and remove the unused MessageSource import. The resolver excludes
all non-user, non-checkpoint sources, so these cases do not depend on goal
identity or need a goal-package dependency. Message content, event order,
captured sequence numbers, and every projection assertion stay unchanged.
Validation: all 49 session-reference tests pass, the Host TypeScript
aggregate passes, and the diff passes whitespace checks.
The release branch still passed resolutionMode: link from the source CLI
and documented a separate link/runtime selector. The integrated master
has removed that selector and its materializer in favor of one runtime
resolver that also supports source launches. The spread kept compiling
because excess-property checking did not reject it, but the property was
ignored by runProfile and the retained prose contradicted the runtime.
Remove the dead option/comment and its obsolete bilingual paragraph.
The CLI entry and the architecture note now match the integrated master;
keep source-module identity verified through its real source-launch and
tool-execution tests. This retires an old integration workaround, without
changing Session V4 or restoring a second profile resolution backend.
The new master source-tool smoke reads isError and content from a V3
nested tool-result block. After integration with the V4 release, that
block no longer exists: the durable message itself has role tool,
toolCallId, isError and result content. The old assertions fail to
compile and cannot verify the current writer's output.
Read the existing V4 fields while retaining the real source-profile
launch, single Tools instance, call identity, sandbox error and shell
output checks. Do not impose the old single-wrapper cardinality on the
flat content array. This adapts the incoming regression test without
changing the V4 format, migration implementation or stored recordings.
Integrate the master tip observed for the final V4 landing preparation on
top of the already merged migration stack. Preserve the V4 writer, flat
message migration, extension policy, catalog validation and historical
recordings while bringing in upstream profile resolution and desktop/UI
changes.
Resolve four generated translation-pairing records from the combined
English/Chinese text. Keep this master checkpoint separate from any
integration fixes and retain all reviewed parent history. Follow-up
validation covers the affected launch/UI paths and migration replay.
The cookbook previously promoted plugin prefixes and owner separation into a general requirement. That overstates the migration policy: direct source kinds and ordinary metadata need no new namespace solely because they are extensible.
Require each edge to document only the mappings its target needs and why, preserve nested opaque data, and permit alpha refusal of unexpected source fields that would otherwise acquire new target meaning. Exact V3 source and ToolSchema choices remain in the V3-to-V4 README; no runtime, schema, or provider behavior changes.
The convention identifies how an edge preserves the identifiers and extension fields that it explicitly maps; it does not require every extension-owned field in every source slot to be renamed.
Keep the cookbook general enough for preserved stream-start extras while retaining the exact field-owner mappings documented by each migration edge. This changes policy wording only, not the current V4 schema or conversion implementation.
This case calls only liftToolResult; the native validator has separate coverage. Name the behavior actually asserted so the test title does not claim both paths are covered by one assertion. Assertions and production behavior are unchanged.
The flat result conversion prefixes every extension-owned field, not just names currently colliding with core fields. Correct the cookbook wording to match that complete mapping.
Record why retaining opaque message and wrapper owners does not invent a shared owner or a new core metadata field. The deliberate costs are retaining duplicate opaque values and requiring plugin consumers to use V4 names; the first-class tool-message decision and runtime implementation stay unchanged.
The exported converter rejects malformed wrappers with SessionFormatError in addition to refusing unsupported nesting. Document both failure categories so callers can distinguish corruption from a converter support limit.
Remove the obsolete claim that recorded replay compares serialized field order: the native-writer parity assertion compares parsed objects. The emitted fields and their order remain unchanged.
An incoming converter runs only when a historical generation is selected. Fixing a mapping after the target ships does not revisit already-produced target files, so allowing compatible converter fixes must not imply repairing those files automatically.
Keep the user-approved ability to extend or correct historical conversion without a target version bump. Require documentation of changed accepted mappings and continued support for existing outputs, and distinguish that work from changing target representation or interpretation incompatibly. Keep operational alpha guidance in the cookbook and the owning decision note focused on the runtime facts.
The parity assertion previously compared a fixed V3 log with whichever generation the ordinary scenario selected. A legitimate V4 re-recording could change call ids and make the assertion compare different model inputs.
Run an additional replay-only variant through the existing headless runner with both its replay file and migration expectation bound to the retained V3 recording. Keep the ordinary scenario on its highest generation, retain call ids in the comparison, and preserve every committed recording.
Validation: the ordinary replay, retained-V3 replay, and old-wrapper negative control pass. Changing only the latest V4 call ids makes the former assertion fail but leaves the corrected retained-V3 replay passing; both fixture and source bytes are restored after this control.
The compatibility obligation belongs to the released target format.
An incoming converter can be fixed or support more historical inputs after
the target ships, as long as its output preserves that target's meaning.
Alpha releases may reject valid historical cases without an implemented,
evidenced mapping instead of expanding the format for speculative inputs.
Record the native-writer oracle, pre-normalization comparison, supported
flat V4 tool-result representation, uniform plugin naming, owner collision
rules, retained source files and read-only private corpus handling in the
migration cookbook. Align the existing architecture note with this rule;
keep native target changes and source conversion fixes distinct.
This updates contributor guidance only. It does not change finalization
checkpoint machinery, existing V4 schemas, or any released codec.
The ordinary snapshot comparator prepares both inputs through the current
format catalog. That can hide a writer still emitting an old tool-result
wrapper: both sides become the same normalized representation.
For the existing stock tool-call-turn replay, explicitly migrate its
retained V3 recording and compare result message fields with the freshly
harvested native log before format normalization. Check both generation
headers and remove only the generated message ID from this comparison.
Do not use the scenario's selected V4 successor as the migration input.
Add a negative control whose header claims the current writer while its
body keeps the V3 wrapper; the raw comparison must reject it. The same
shipped headless profile and stock bash tool provide the positive case.
Keep all recordings and their selected generations unchanged.
The ordinary V3 writer records a user-role message containing one result
wrapper. V4 writes the same tool output directly in a tool-role message.
Keep that native V4 representation instead of introducing a nested message
content type for hypothetical historical shapes.
Preserve extra historical fields when removing the wrapper. Prefix outer
message fields with plugin:message: and result fields with plugin:result:
so equal names retain their original ownership and cannot overwrite the
interpreted call ID or error flag. Include the complete old name, even
when it is already prefixed, and preserve every JSON value and own key.
Only the validated wrapper supplies the interpreted result fields.
Leave unimplemented nested-result conversion as an explicit refusal that
preserves its source. Describe this as the converter's current limit, not
an impossibility or a reason to extend the established V4 format.
Cover owner/name collisions, opaque special keys, absent error flags,
unchanged sources and strict native reopening through plain and compressed
read/write persistence. No current message type or schema is added.
Isolated complete/incomplete-child tests did not distinguish skipping one child
from prematurely stopping the entire candidate loop. A stop could therefore
silently omit a later child's fully supported catalog fact.
Pair a complete child with an older unavailable child so deterministic ordering
processes the unavailable evidence first. Cover missing, unsupported-version,
and multiple-descriptor evidence, requiring exactly the complete child's fact
with the correct sequence and timestamp.
A temporary continue-to-break mutation failed all three new cases. Restoring
the implementation passed the focused 19 cases and the complete selected owner
run: 428 passed, one compression-specific skip, with 100% coverage on facts.ts
and migration.ts. This commit changes tests only.
The child-catalog README did not record the lasting missing-entry consequence
under Known Limitations, and its validation wording implied that duplicate or
unsupported descriptors had their discovery fields checked. The converter only
checks those fields when exactly one supported own descriptor is available.
Specify that distinction, retain creation-time checks for existing entries, and
state that current V4 reads do not rescan children to repair an omitted entry.
Document the first-descriptor composition reader and last-descriptor identity
projection accurately: multiple own records violate the establishing provider's
exactly-once rule, so the backfill policy does not choose between those consumer
policies. This corrects the rationale without changing migration admission.
Update the paired English/Chinese package docs and decision note together.
Validation: doc-sync 41/41, full lint, and owner tests passed; production source,
frozen generations, persistence schema, and recorded fixtures are unchanged.
A readable V3 child may have no own descriptor, an unknown descriptor version,
or multiple descriptors. Those states do not provide a unique discovery fact,
but they do not make the parent or the retained child history unreadable.
Rejecting the entire parent prevented otherwise valid V3 Sessions from opening.
Append a missing parent catalog row only for exactly one supported own child
descriptor. Keep existing parent entries and every child record when no fact is
available. Retain rejection of mismatched creation times, unambiguous mode/label
conflicts, malformed known fields, unreadable membership and revision drift.
Cover raw and compressed read/write publication, preserved source bytes, late
descriptor availability, inherited descriptors and Preview packing. Document
that unavailable discovery evidence can leave a child absent from its parent
catalog and does not trigger automatic backfill after the V4 successor exists.
Extracts the child-evidence behavior from 7ba7e174dd1 and the Preview regression
from 9c85eb60bd25, preserving the original migration authors' work while keeping
message representation and extension interpretation outside this commit.
Co-authored-by: Dudu-0223 <[email protected]>
The V3 writer on master declares and emits only name, description and
parameters. deferLoading is a first-class V4 persistence addition whose
provider behavior is not implemented yet. Prefixing every historical extra
ToolSchema key changes ordinary plugin metadata without a format need.
Preserve all ordinary tool-definition keys and JSON values unchanged,
including vendor metadata, own __proto__ properties, already-prefixed keys
and nested parameters. Remove the general key-renaming mapper. Reject only
an own top-level deferLoading field in V3 input: there is no established V3
meaning to translate, and the alpha converter may refuse unsupported source
forms rather than invent compatibility semantics. Leave native V4's
optional-true declaration, admission and provider refusal behavior intact.
The regression tests fail on the old mapper for metadata preservation and
unsupported-field refusal. Physical plain/zstd read/write tests verify source
bytes and identity remain unchanged and no V4 successor appears on refusal;
controls preserve nested parameters.deferLoading and native V4 true values.
No Session format/schema change, frozen codec edit, or historical recording
version bump is needed.
Convert an unknown legacy plugin wrapper such as
{kind: 'plugin', plugin: 'acme'} into {kind: 'plugin:acme'}, retaining the
complete plugin name after one prefix. Keep the existing explicit
first-party producer renames and preserve all non-identity metadata.
A direct source already records its producer in kind. Reclassifying
unknown direct kinds into plugin:source:<name> needlessly changes that
attribution and forces plugin consumers to adopt new identifiers even
though V4 accepts their existing representation. Return every valid
direct source unchanged, including already-prefixed names. Remove the
obsolete direct-kind table and two-category identity helper rather than
adding collision classification or another mapping layer.
Update the source conversion examples, codec/restore regressions, and
native fixture producers together. Refresh only the owning Python
advanced scenario's unshipped V4 outputs; preserve historical V0-V3
recordings and their generation selection. Event names, content tags,
and ToolSchema migration remain separate concerns.
Refs #2692
The generation note keeps generation for succession only and names the table, the interception, and the routes with the code's words; the CLI reference, Desktop, plugin-package-inventory, and loader-smoke READMEs, the bundled-runtime, agent-teams, plugin-bundles, and source-launch notes, and the client AGENTS guide follow. Pairing records re-recorded.
The Consumer-owned strictness bullet names the Loader subtree that agent presets own instead of the runtime resolution; the service field, its file comment, the dispose JSDoc, and the route JSDoc use interception; the native-after-interception route documents both anchors it can carry; stale module-fallback comments name the runtime resolution; importFromResolution matches its parameter.
The CLI derives the installation anchor from import.meta.url and the Desktop Host builds it from its runtime directory; neither is a symlink, so the installation root is kept as given. The note and the traversal comment say so instead of attributing every launcher to import.meta.url.
The numbered chain lists directories only; the legend for ①-⑤ moves into prose so both language sides can read it while the code block stays byte-identical for the pairing gate.
The lookup-order note records that the launcher derives the installation anchor from import.meta.url, so bundle discovery and dependency traversal start from the same real directory.
Notes and the app-boot README name the startup package table the runtime resolution and the installed hook the interception; generation remains the word for the additive successor semantics of replace().
A profile with the legacy directory but no node_modules, and a symlink whose target parent is gone, exercise the remaining branches of removeLinkProjections; the non-ENOENT realpath failure is a host filesystem fault and is excluded from coverage like its siblings.
The columnTransition probe raced data-animating's transitionend/600ms
settle: checkpoints recorded whichever side of the window the capture
landed on. Waiting for the attribute to drop pins the steady state —
no declared transition (computed 'all') — deterministically.
removeLinkProjections unlinks the symlinks under a profile's node_modules whose target lies inside <profile>/.dsh-module-fallback/node_modules and then removes that directory; pnpm-installed packages and every other symlink stay. loadProfile and the Desktop applyRelease call it once per launch. Regression: with bundle A installed but deselected and bundle B selected, B's same-named plugin resolves instead of A's projected copy.
The lookup-order note, the cleanup-removal note, and the app-boot and Desktop READMEs state that profile load removes the .dsh-module-fallback projections the dsh 0.1.5 releases wrote into a profile, so a deselected bundle's projected plugin no longer shadows the same-named plugin the generation selects from another bundle.
Drop the fullscreen panel's whole-box no-drag: it swallowed the strips'
blank-run window drag and made no-drag the steady state, turning the
open/close app-region nudge into a computed-value no-op (electron#32341)
exactly when a hidden panel opens straight into fullscreen. The panel now
declares no steady-state app-region in either presentation, and ui-dockkit
subtracts the one box base.css never matches — the plain-div split divider —
alongside tabs, strip-end chrome, and pane bodies. New specs pin both
contracts against the stylesheets on disk.
The add card's panels are now derived from the shown mode and the modes
visited since it opened, and each renders only while its mode is still
offered, so a refresh that changes which modes are offered — every
configurable row gone, the pi-ai namespace unmounted, a route freed
after the catalog was exhausted — can neither leave the card without a
panel nor keep a form whose write the Host would refuse. The catalog
draft resolves to the chosen row while its namespace can take the
write, else to the first addable row; closing either panel forgets it,
so a later refresh cannot open a row editor unasked.
Both cards report a write or an endpoint interrogation in flight, and
the switch and the catalog select lock while one is, since a switch
underneath it would orphan the answer or the picker it opens. The
SegmentedControl takes the owner's id and pairs each tab with its panel
through `aria-controls` and `aria-labelledby`; a card offering one mode
carries that mode as its title instead of an orphan tabpanel, and a
locked segment names its reason as hover text. `fullWidth` goes: no
consumer asked for it.
The new "添加模型提供商" button shares a prefix with the Models nav entry,
so every e2e that clicked the nav through a non-exact locator resolved
two buttons once the page rendered; the nav locators are exact now.
The vendor sense of "provider" is registered in the terminology table
as 模型提供商 with its scope — the Models settings copy, the providers
user guide, and this package's README — while the generic provider rows
keep 提供方, and the package's Chinese README uses the vendor term
throughout. The custom card's header comment no longer claims a mode
switch that a custom-only card does not render, and the SegmentedControl
suite gains a positive Home/End case whose first enabled option is not
the first option.
The new vocabulary fixture helper set cwd to a private repository but still
inherited GIT_DIR, GIT_WORK_TREE and GIT_INDEX_FILE. A negative control with
two disposable repositories demonstrated that git add/commit changed the
foreign repository's HEAD instead of the intended fixture repository.
Remove only those three location variables from the child environment.
Leave the caller's environment untouched and retain the existing identity,
hook and signing overrides. The regression checks the foreign HEAD and work
tree remain unchanged while the intended target commits its private file.
This is confined to the test helper; it adds no production Git abstraction
or Session-format behavior.
The preserved direct-source set is the predecessor migration's accepted set
without the plugin wrapper. It includes older coordinator and subagent-report
identities even though those names have no current producer declaration.
Describe that historical admission fact instead of implying every retained
identity has a live V4 owner. No mapping or runtime behavior changes.
Central V4 event admission already rejects tool-result content in Team
queue records, but projection checkpoints use the Team state schema directly.
After restoring the baseline Team parser during extraction, the production
SessionProjectionRegistry.restore() path accepted a decoded native V4
checkpoint containing the retired wrapper without reading an event.
Remove only that V4-incompatible schema arm. Keep the existing unknown-JSON
parser unchanged; its independent own-key preservation fix remains in the
standalone master PR, where recursive V3 tool results are still valid.
Exercise the real checkpoint restoration recipe with a valid text state and
an invalid retired-result state, plus queued-event projection. The new
checkpoint rejection failed before this change; central event admission and
the complete Team owner suite pass afterward with 100% projection coverage.
No Session schema, format version or projection state version is changed:
the rejected value is already outside the native V4 content vocabulary.
Retaining own keys in the Team projection/checkpoint parser fixes a generic
JSON reconstruction defect. It is independent of V3-to-V4 identifier
conversion and can be reviewed directly against master, where recursive V3
tool-result content is still valid.
Restore the Team implementation, tests and paired README to this layer's
parent after the standalone owner captured the exact delta from 85737364be9.
The standalone port retains V3 validators and applies only the non-cloning
unknown-JSON repair. Migration content handling for team/message/queued and
all approved namespace, delivery and source-admission rules remain intact.
This removes an independent runtime/parser policy from the format stack;
it does not change any Session schema, codec, version or stored source file.
The event-type allowlist and opaque-payload attachment policy change live
Session authorization/export behavior independently of a format upgrade.
They belong in a focused PR against master, where supported native V3 nested
tool-result content must also be handled deliberately.
Restore the session-controller and archive implementation, tests and README
pairs to this layer's parent. Remove the test-only session-format-catalog
dependency and its lock entry with the migration-specific attachment tests.
The original changes are captured from immutable commit 85737364be9 for the
standalone work; migration identifier conversion and reader checks are intact.
This extraction keeps the integration stack's existing attachment behavior
until the separate policy change is reviewed. It makes no Session-format,
schema or version change.
The physical V3 codec admits an unclassified event envelope without deciding
whether its type is required. The V3-to-V4 stage then used shared message
walkers and target vocabulary, so an invalid required developer/message could
acquire the V4 developer role although historical V3 restoration rejects it.
Generic required unknown events were also left to the wrong version's reader.
After preserving explicitly ignorable extensions, require every remaining
source type to belong to the fixed V3 vocabulary before interpreting payloads.
This is a fail-loud check for invalid or nonconforming source data, not new
compatibility handling for third-party events, which carry ignorable:true.
The approved plugin identifier conversion and native V4 readers are unchanged.
Exercise a complete developer-event sequence through direct migration,
historical/current catalogs, ignorable conversion and native V4 restoration;
add a generic unknown type. Both negative controls accepted the invalid source
before this guard. Plain/zstd read/write regressions verify refusal preserves
the source generation and never publishes a partial V4 successor.
A V3 delivery marker claiming generation 4 would become active when its
header advances to V4. The integration baseline already refused that case.
Renaming every higher-generation marker instead introduced a general
conversion policy for future formats that this V3-to-V4 edge does not own.
Restore the targeted generation 4 refusal. Keep other delivery event names,
payloads and coordinates unchanged, including generations 5 and 99, which
remain inactive in V4. Their future activation checks belong to a concrete
later format edge. Preserve the separate plugin-event identifier policy.
Cover read/write opens for both plain and zstd storage: target-generation
refusal preserves source bytes and publishes no successor, while later
markers survive V4 publication and native reopening unchanged. Update the
snapshot-normalizer cases and document the alpha converter coverage limit:
unsupported histories may fail without source mutation, and later converter
fixes may expand support without a new version when V4 output stays compatible.
V3 and V4 declare the same block-start fields: type, index and blockType.
The migration's identifier policy can qualify an extension blockType, but
prefixing every additional field has no current V4 field collision to solve.
That rewrite changed captured keys to anticipate hypothetical future fields.
Retain every non-blockType field and value exactly, including already
prefixed names and own JSON keys. Keep the approved blockType identifier
prefix and the separate ToolSchema field policy unchanged. Update the local
conversion tests to verify both known and extension starts retain metadata
and to use the actual raw-record time field. Later field-specific conversion
belongs to a concrete source/target change instead of this edge.
The JSONL retired-content admission matrix covers both historical V3 input
and native V4 input across read/write and plain/zstd storage. V3 migration
now prefixes unknown ignorable event identities with plugin:, while native
V4 reads retain their stored identity. The shared expectation still required
the unchanged V3 event tag and caused four deterministic CI failures.
Select the expected event type by input generation while retaining the full
payload, envelope, and immutable source-byte assertions. This corrects the
test for the intended migration without weakening native admission or
changing runtime behavior. The focused 20-case matrix now passes; restoring
the old expectation reproduces the same four failures locally.
V3 permits external event, source and content identities and additional
request-tool fields. Copying those values into V4 can give an old plugin
named tool a core source identity, turn old tool-addition content into a
registry instruction, or activate formerly ignored deferLoading and delivery
metadata. Rejecting such collisions would instead refuse valid source logs.
Keep historical extensions in the existing extensible JSON representation.
Prefix unknown content and ignorable event tags with plugin:, preserving
all other content fields and opaque event payloads. Prefix external source
identities with plugin:plugin: or plugin:source: so wrapper and direct kinds
cannot collide even when original names already contain a category prefix.
Known first-party mappings remain explicit and unchanged.
Prefix extra request-tool field names and block-start field names with
plugin:, retaining exact values and existing-prefix doubling. Core schema
parameters, arguments, replay state and nested plugin data are not traversed.
Keep future-generation delivery records inactive after header promotion.
Pin the V3 event vocabulary independently of the current writer and provide
an offline verifier against an explicitly selected historical source commit.
Attachment authorization and export inspect declared event content only.
Team checkpoints preserve all own keys of unknown JSON blocks while still
rejecting malformed known content and retired core tool-result blocks.
Exercise physical read/write/reopen, prefix injectivity, own __proto__ keys,
provider opacity and retained source-identity snapshots.
No generic extension block, metadata wrapper or new schema-tooling type is
introduced. V4 type declarations, persistence schema, checkpoint and frozen
V0-V3 codecs remain unchanged. Canonical tool-result lifting and incomplete
child-catalog policy remain separate concerns.
Adapted from the extension-preservation work in 7ba7e174dd1 by Tianyi Cui.
Co-authored-by: Tianyi Cui <[email protected]>
The cache reader preserves opaque JSON keys, but each projection still
parses the returned checkpoint through its own stateSchema before hydration.
State that ownership explicitly so documentation does not imply this reader
fix also changes projection-defined transformations or cloning behavior.
Distinguish the synthetic V7 StorageDomain regression fixture from released
cache archives. Describe its read, put and reopen path directly, and group
its JSON validation checks separately from archived-version recovery.
Translate the Chinese Agent Note headings and keep the paired docs aligned.
The note retains the borrowed-value tradeoff and links the independent
predecessor-format and lifecycle-identity decision.
Production expressions and all test assertions remain unchanged; only
contracts, fixture descriptions, test labels and grouping are clarified.
The fix does not add another persistence format or broaden migration scope.
The projection-cache writer preserves lossless JSON, including own keys named
__proto__. The z.json reader rebuilds objects and drops those keys, so opening
the same stored checkpoint can produce state different from Session replay.
This affects embedded result metadata and opaque extension payloads as well
as other projection state.
Validate with the existing isJsonValue predicate used by the writer's
snapshotJsonValue rules. It rejects non-JSON and lossy values without
rebuilding valid borrowed records. Keep the stored representation and domain
version unchanged: this is a reader correction, not a new cache format.
Exercise a fixture captured through the actual pre-fix writer with a real
StorageDomain open, rewrite, close and reopen. Preserve nested prototype-named
keys and retain negative coverage for invalid JSON values. Document why this
validation matches the storage-domain immutable borrowed-value contract.
Use neutral arbitrary JSON in the new opaque-state fixture so this reader
regression stays independent of Session message representation. Its own
prototype-named keys and nested arrays are unchanged in purpose. Confirm
the fixture matches the pre-fix StorageDomain writer output and that the
old z.json reader fails its key-preservation assertion. No released Session
recordings or historical cache fixtures are edited.
Cold current-generation reopen freezes the complete decoded event graph
before publishing shared-frozen history. Compact assistant streams carry
125,000 text values and 124,600 time deltas in the required benchmark;
for-in traversal enumerates their array indexes as string property keys.
An open-only profile attributes about 4.2 ms to complete event freezing.
Use numeric indexing for arrays produced by JSON decoding and the fixed
migration catalog. Those arrays contain indexed JSON values only. Keep
every Object.freeze call and every nested-object visit, preserve the
object-key path including own __proto__ and constructor data, and never
infer deep immutability from a shallow Object.isFrozen result. No decoding,
admission, relationship validation, or freezing moves past open().
The unchanged five-sample V4 reopen median falls from 22.6 to 17.7 ms on
M4 Pro / Node 24.19. Restoring and rebuilding the exact old traversal gives
21.7 ms. First-history and resume also improve; Agent retained heap stays
4.5 MB and every 128 MB case completes. Keep the existing 63 ms gate,
fixture dimensions, cold-process setup, and all other budgets unchanged.
Local measurements establish removed work; hosted CI remains the required
runner verdict.
Cover nested opaque arrays and objects, special JSON keys, rejected
mutations, and shared event identity through historical V3 and native V4
reads. Record the before/candidate/restored samples and the local-versus-CI
limit in the existing performance note.
Merge master 7e2b049d8e2523127a3b4fba38cab4748ef0c72f, observed on
2026-09-19 at 07:38:37 UTC, into release/session-log-v4 at
c4663e8671e33bc2cfdbc868b0b18d979508d39b. Keep this base integration
separate from the five persistence repair PRs so their review diffs
remain focused and can be rebased without moving their fixes here.
Combine master's pin ordering and archived-session filter with V4's
parent-owned direct child catalog. Both grouped and flat sidebar rows
must pass the Session list to the V4 activity calculation; restoring
master's summary-lineage traversal would reintroduce the removed
subagent ownership and count unrelated descendants.
Keep master's removal of the archived-session settings page. Its only
release-side test change renamed a Session list fixture field, so no
runtime behavior is lost by deleting that obsolete test. Adapt the
release-side direct-child-status regression to the new row/filter
arguments and preserve its assertions.
Retain master's workspace ordering and row-motion documentation while
keeping the V4 child-catalog wording. Reconcile the generated bilingual
records against the merged documents. Frozen V0-V3 codecs, recorded
Session JSONL inputs, and the current V4 persistence schema are unchanged.
Refs #2692, #3273
Publish --dsh-frame-top-clearance on html[data-platform='darwin'] so
body-portalled overlays inherit it; useAnchoredMaxHeight and the portalled
Menu widen their 12px viewport margin to it, Modal centralizes the vertical
padding its cards previously each computed, and the trigger menu keeps an
84px margin on every platform so it stops below the conversation header.
Reduce Transparency replaces the sidebar tint with a 90%-opaque fill.
Resolution routes are native, generation, and native-after-generation: Node keeps the importer's lookup, answers from the generation entry at the interception layer, or resumes its chain at that layer. The profile module collects installation-scope and profile-scope packages and reads package manifests under those names; the generation types describe the interception layer instead of a fallback table.
Windows tmpdir spells the user directory with an 8.3 short name. Generation anchors and Node's own resolution report the long name, so the resolver, generation, and real-launch fixtures now create their roots through realpathSync.native and compute expected paths the same way.
The Models page offered two side-by-side buttons, "Add provider" and
"Add a custom provider", which asked a new user to classify their
provider by an implementation fact — whether it is in the installed
pi-ai catalog — before seeing anything. One "Add model provider" button
now opens one card whose segmented mode switch chooses between a
third-party model provider (the catalog select over the provider
editor) and a custom model API (the create form for a relay, a
self-hosted server, or any other OpenAI- or Anthropic-compatible
endpoint), with a one-line purpose beneath the switch. A panel mounts
the first time its mode is shown and stays mounted, hidden, while the
card is open, so switching modes discards neither draft.
A mode is offered only while its namespace is mounted and disabled
while it has nothing to offer, which keeps the old per-button
availability; with one mode the card shows that form alone. The
protocol pickers name each protocol by its product name while the
stored value stays the schema identifier, and an unnamed protocol shows
that identifier. The Chinese copy of this page calls a provider 提供商
throughout.
The e2e scenarios drive the single entry and its custom-API mode, and
the ARIA goldens that carry the page copy are re-recorded. The user
guide describes the one entry with fresh screenshots in both languages.
A tablist of two or more equal-width segments with one sliding
indicator, for switching a card or panel between a few mutually
exclusive modes. The owner holds the selection; `label` names the list
and every segment label is owner-localized. Arrow keys, Home, and End
walk the enabled segments with a roving tabindex, and the keyboard
follows the selection once the owner adopts it.
Segments are equal grid tracks sized to the widest label when the
control hugs its content, which is what lets the indicator be placed
arithmetically from the segment count and selected index rather than by
measuring the DOM. The indicator draws its stroke inside the soft
elevation shadow, as every elevated surface does.
A simplification note records why production Desktop no longer cleans core-package copies out of the profile, keeps the 2026-09-15 motivation and rejected alternatives, and names the capability given up and the reintroduction conditions; the superseded 2026-09-15 note is consolidated into it. The bundled-runtime note drops its cleanup qualification paragraph, and the Desktop README describes launch preparation without the cleanup step.
routeScoped names the profile parent whose node_modules is the interception layer. The installation traversal records why the installation root keeps its caller-supplied directory and why generation construction keeps its Promise return type.
The generation note and the app-boot README no longer claim that runtime lookup hides the shared position or recognizes profile projections: generation entries occupy their package names at $DSH_HOME/profiles/node_modules and every other name sees that directory as an ordinary ancestor. Pairing records re-recorded.
Translate the profile lookup-order Agent Note section for section and record its pairing. The desktop-web-wrapper note names the runtime resolution generation instead of module fallbacks and drops the one-time desktop-runtime-state cleanup that no longer exists.
The generated catalog is a bilingual pair, so the regenerated English side
must be brought over with its Chinese counterpart before the pairing record
is re-written.
isPackagedExecutable has one caller in its own module; the installation traversal comment names the runtime resolver instead of the removed fallback directory.
The CI Windows runner reports an empty \0 for a PowerShell-level
'>nul' redirection around the cmd call, and PowerShell's '>nul' opens a file
named nul rather than exercising cmd's redirection. Pass the whole command
to cmd so it parses '>NUL' itself and prove it with cmd's own output.
The inheritable `Deny Everyone FILE_DELETE_CHILD` also reached FILES, and
0x40 is a member of FILE_ALL_ACCESS: measured against a real runner, a
Medium caller's CreateFileW(GENERIC_ALL) on every file, directory, and temp
root inside a granted root returned ACCESS_DENIED. The deny now carries
CONTAINER_INHERIT_ACE, so files keep working; a FullControl open of a
DIRECTORY inside a granted root stays denied and is pinned as the residual
cost of denying a right the full-access mask contains.
Also from the review:
- Leaks on fail-closed paths: the label ACL on both early SetEntriesInAclW
exits, the read descriptor when the label build fails, and the parsed
capability SID when either well-known-SID creation fails.
- `revokeWrite` keeps the shared Low label while another capability grant
remains on the directory, so two grants on one path stay independently
revocable; `hasExactLabel` delegates to hasExactEntry.
- The delete probe uses process.execPath, restrictTokenIntegrity guards a
zero-length SID, and verify/abi-probe.cpp covers the new constants
(compiled; static_asserts pass).
- Consumers updated in the same change: sandbox-local source comments and
README pair, pwsh-sandbox README pair, docs/subsystems/sandbox pair, and
the 2026-07-06 sandbox note pair no longer cite the Everyone-write
boundary as a current partial reason.
- New Agent Note records the mandatory-integrity decision, the deny scoping,
the standing label with its externality (other Low-integrity processes
running as the user, surviving the session), and the rejected
alternatives; the 2026-08-08 note cross-links it.
- New runner pins: NUL stays writable under the Low token, the FullControl
file/directory split, and the shared-label revoke. Per-file coverage back
to 100%.
Remove cleanProfileCorePackages and migrateDesktopProfileLinks with their specs; applyRelease keeps runtime validation, settings migration, and profile creation. A regression pins that installed packages, declarations, legacy state, and the lockfile survive a launch.
The src/lib x installed/npm-link fixture adds a stale @deepseek-ai/dsh-tools link in profiles/node_modules, a plugin dependency absent from the generation with nested external leaves, and byte-for-byte preservation of profile files and link targets.
The generation stands in for $DSH_HOME/profiles/node_modules/<name> only for names it carries; other names see that directory as an ordinary ancestor. The interception layer is the first ancestor node_modules above the importer's profile directory, so application-owned profiles follow the same rule. Stale .dsh-module-fallback projections are ordinary symlinks: their shape recognition leaves the resolver, the local package names, and the bundle dependency traversal.
The resolver spec gains a table-driven matrix over importer, package kind, ancestor layers, and directory or symlink form, with readable fixture names.
Add the Chinese Agent Note describing profile resolution as Node's ancestor node_modules chain with the generation occupying the package directories at $DSH_HOME/profiles/node_modules, the ESM/CommonJS, web/desktop, and npm-link scenarios, and the developer onboarding guide.
whoami /groups reports the mandatory label on this workstation but produces
no matching line on the CI Windows runner, so the assertion pinned the host
rather than the product. The lowered integrity level stays covered by the
unit pin on restrictTokenIntegrity and by the behavior that only holds with
it: the Everyone-Modify denial, the outside-delete denial, and the
cross-root delete denial. README records the cmdlet as host-dependent
diagnostic noise again.
The merged dependency repair in #4589 uses a recorded Launchpad build,
but #4591 also requires a regression guard against restoring the live
Ubuntu package pool. Those pool URLs disappear when versions are
superseded, even when the content digest is pinned.
Adapt LegGasai's URL guard from closed PR #4590 to the build-specific
endpoint that landed in #4589. Check the endpoint structure without
copying the current build number, version, or checksum into the test,
and reject Ubuntu pool downloads anywhere in the script so adding a
pool fallback cannot bypass the guard.
Negative controls restore the original archive URL and inject security
and ports pool fallback URLs; each fails the new guard. The original
script is restored byte-for-byte before all 44 workflow tests pass.
This adds only a static regression test, with no network calls or
changes to the download script or CI execution behavior.
Co-authored-by: LegGasai <[email protected]>
Hovering a session row again scrolls a clipped title to its far edge
(the #4423 reveal), but overflow of at most 8px stays put so barely
clipped titles do not jitter. The hover mask fade yields back to plain
clipping — at the scrolled extent the fade would cover the revealed
tail. Row action buttons tighten from gap 12 to 10.
Hovering a session row no longer scrolls its clipped title (the mask fade
stays), the one-time archived-filter callout and the content-search
unavailable notice are gone, the group-row new-session button shares the
new-chat glyph, and the view-options trigger uses the two-slider icon with
a 200px menu floor and a visible separator hairline.
Pin sessions to the top of the sidebar and reach archived sessions
through the view-options filter instead of a settings page; archiving
gains an undo/filter toast, and row motion stays reserved for pin jumps
and archive fades.
Include the user's merge of #4589 so Linux CI uses the available, verified
Ubuntu bubblewrap 0.12.0 build instead of the pruned 0.9.0 archive URL.
The user explicitly authorized this CI-prerequisite refresh without waiting
for the ordinary hourly master-following interval.
Preserve the reviewed upstream commit and exact one-file patch. The recorded
Launchpad build URL, checksum, extraction and namespace probe match #4589,
whose supported Ubuntu 24.04 preparation and execution checks passed. Bash
syntax and merge whitespace checks pass; no product runtime, Session format
or snapshot generation changes are introduced by this merge.
Ubuntu removed the superseded 0.9.0-1ubuntu0.1 payload from its live
archive pool, so the shared Linux CI preparation step fails with HTTP
404 before tests start. Address Ubuntu's recorded Launchpad build so
future pool pruning does not invalidate the pinned download location.
Select upstream's 0.12.0 security release for CVE-2026-87766 instead of
Ubuntu's isolated 0.9.0 backport, whose regressions and support limits
are documented by upstream in Launchpad bug 2166359. Keep checksum
verification, direct extraction, and the existing namespace probe.
Verify build 33546835 against its published SHA256 and archive SHA512.
Its amd64 ELF requires at most GLIBC_2.38 and x86-64 baseline; its
packaging requires Linux 5.10. Ubuntu 24.04 provides glibc 2.39, Linux
6.8, and the required libcap2/libselinux1 versions. This changes only
the CI helper's pinned payload, not product sandbox configuration.
Include the user's merge of #4586 so the V4 integration and its child PR
exercise the corrected spill-policy synchronization tests. Preserve the
upstream commit and authorship and keep this master checkpoint separate from
migration changes and the remaining benchmark investigation.
The merge changes only the spill-policy test file. Its added and removed
lines match the reviewed upstream fix exactly, while the release branch's
existing V4 fixture adaptations remain intact. No runtime source, persisted
format, or snapshot generation changes.
Validation: all 31 spill-policy owner tests pass with 100% package coverage.
The application phase's map-form combo URL passes the 3 KiB limit once
the four settings companions join the composition, so the module system
partitions that phase into two combos beside the bootstrap one, as the
loading model allows. The smoke pins the three immutable batches and
still checks every combo's script, map, and cache header.
The crumb, uninstall, and configure controls no longer render an icon child in the accessibility tree; the bundle page golden this branch added follows the row and official goldens master re-recorded.
The slow-backend regressions start real NodeRuntime subprocesses, but
vi.waitFor silently imposed a one-second deadline on normal scheduling and
tool execution. Under concurrent Linux and Windows coverage this rejected
valid runs before the tested spill or dispatch stage was reached.
Resolve promises at the actual save-entry and dispatch-start observations,
and fail immediately if the run completes before those observations. Queue
all three requests up front in the bounded-backlog case so a missing third
start tests backpressure rather than child request timing. Cleanup releases
both queued and future spill gates and awaits execution completion.
Keep the existing outer test deadlines, product limits, result assertions,
and ordering checks. A temporary 1.2-second tool delay reproduces both old
failures and passes with these tests; temporarily removing the actual
backpressure wait still fails the third-dispatch assertion. No experiment
or product-code change is included.
Fixes#4585
Keep the master refresh separate from the V3-to-V4 lossless-conversion and
checkpoint JSON fixes so the release baseline and each feature change remain
independently reviewable. This checkpoint extends the already-published
4435f6f519 merge before the #4583 child is rebased.
Integrate exactly master 5ec3ffe337062ab5bb4955933008afe8d0a0b915. Its #4156
changes add watched file/document refresh, filesystem watch readiness,
directory expansion restoration, and chat bottom-follow fixes. They update
shared filesystem and Client consumers without changing Session format or
snapshot generation.
The merge has no conflicts. Its 114-file scope and line counts match upstream;
code and prose additions/removals are unchanged. Only the two translation
pairing records incorporate the release branch's retained V4 documentation.
Validation: 762 owner tests passed (1 platform skip), 11 focused Web tests
passed, full build passed, and all 41 doc-sync checks passed. Runtime closure
and Client package checks passed; fs-local publint completed with the existing
source-wildcard export warning.
One home per fact: the parent-FILE_DELETE_CHILD rationale now lives in the
acl module doc and the package README instead of being retold in
restrictTokenIntegrity, init(), and grantWrite. Drops the duplicated
inline narration; behavior unchanged.
The Low integrity label confines writes and deletes to the labeled roots,
but every granted root is labeled Low, so the integrity check alone still
let one confined child delete inside ANOTHER granted root (another
workspace, a live sibling temp directory): Windows authorizes a delete from
the parent directory's FILE_DELETE_CHILD right, which the write-restricted
intersection never reaches.
Each grant now also denies FILE_DELETE_CHILD to the world SID in the same
SetNamedSecurityInfoW call, so the capability ACE's DELETE bit is the only
delete authority inside a granted root. The idempotent skip additionally
requires the exact deny, so a root granted by an earlier build receives it
on its next provision. Delete paths inside the roots keep working; a file
whose own DACL grants no DELETE is no longer deletable through its parent's
rights, for the confined child and for the user's own processes alike.
The write-restricted token's capability-SID intersection only covers the
write bits of the object's own access check. Windows also authorizes a
write or a delete from the parent directory's FILE_DELETE_CHILD right, and
no restricting SID has to co-sign that authority: the confined child could
therefore delete any file its ambient user SIDs control. `cmd /c del`
outside the workspace succeeded under BOTH modes, and .NET
File.Delete/DeleteFileW did too wherever the ambient DACL held Modify.
Lower the restricted token to Low integrity and apply a Low no-write-up
mandatory label to every granted directory in the same SetNamedSecurityInfoW
call that adds its capability ACE. The kernel evaluates the integrity policy
inside the access check whichever right supplied the authority, so the
granted roots stay writable while everything else becomes delete-proof as
well as write-proof. The label inherits to new children and follows the
existing standing/revocable grant lifecycle.
Also closes the Everyone-Modify ambient write boundary, and pins the delete
paths (cmd, .NET, Remove-Item, libuv) in runner.spec.ts.
Bring the Browser guide subtitle simplification from master
b605980eb049ce2cc578cdb608f7f8dee66f7b56 onto release checkpoint
2958f0f5cf67f81b18ad690a82c68c606f246d0c.
Keep this upstream synchronization in its own merge commit so the release
refresh can be reviewed independently from the lossless tool-content
conversion. The conflict-free merge contains the exact two-file upstream
patch: shorter English and Chinese guide subtitles and matching assertions.
Validation: all three ui-sidebar-browser apply tests pass, scoped type-aware
lint passes, the Client UI locale gate passes, and the first-parent patch
matches the pinned master delta from 0e9385ad8b in both full-diff and stable
patch-id comparisons.
The accepted-baseline tests inventory temporary files with keys produced by
the host path.join. On Windows those keys contain backslashes, but the status
lookup and acknowledgement-preservation filters use repository-style slash
paths. Windows coverage therefore reads undefined for the status assertion;
the filtered preservation loops also select no files on that platform.
Use posix.join for logical inventory keys while keeping native path.join for
filesystem access. All platforms now exercise the same status lookup and
preservation assertions. This is confined to the test helper and changes no
production admission rule, compatibility policy, schema, or recorded fixture.
The retired-content guard runs on parsed physical JSON before normal event
admission so recovery cannot suppress a retired wrapper after another invalid
row. At that point type and sequence fields are still JSON values rather than
validated event fields. Implicit interpolation therefore violates the typed
lint rule on the final combined branch.
Use explicit String conversions only for the error subject. Keep the guard's
ordering and refusal checks unchanged; requiring an already-valid envelope
here would weaken the pre-recovery check. This does not change the stored
representation or add a migration rule.
The V3-to-V4 README described the main representation changes but did not
enumerate the complete admission and preservation rules. That left reviewers
without a package-owned reference for supported histories, child evidence,
native validation, or cases that must fail without publishing a successor.
Document historical conversion separately from native V4 admission, including
source renames and collisions, first-class tool results, child catalogs,
delivery metadata, retained identities and references, lifecycle checks,
developer-message constraints, fork behavior and recovery entry points.
Explicitly identify the retired-wrapper refusal and the limited scope of
general content validation rather than implying a complete payload validator.
Require this transformation/admission catalog in the version-addition
cookbook so future adjacent migrations document each supported mapping,
unchanged representation, refusal and preservation obligation. Preserve the
accepted-baseline policy: compatible V4 additions use new acknowledgements;
only breaking changes require a new writer version.
Released V3 accepts tool-result wrappers in ordinary user content. Leaving
those wrappers in a V4 artifact violates its declared content types and
reaches provider serializers that cannot represent them. Converting such
rows would require a new event, role, or source ownership decision.
Reject retired result tags only in known interpreted content slots and
raw Assistant stream block starts/ends. Reuse the native retired-syntax
admission path before recoverable suffix suppression and target artifact
restoration before successor publication. Preserve the existing system
and tool-result validators and the canonical V3 tool/result wrapper lift.
Known ignorable developer rows receive the same check during encoding;
unaware physical readers still defer their opaque developer payloads.
Tool arguments, replay state, schema parameters, nested metadata, and
unknown ignorable event payloads are not recursively scanned.
Real JSONL regressions cover V3 migration and native V4 read/write in
plain and Zstandard storage, including a corrupt native prefix. Refusals
preserve source bytes and the sole generation; opaque payloads survive.
No declared schema, frozen V0-V3 codec, or snapshot generation changes.
The integrated V3-to-V4 records contain the header increase that authorizes
their breaking changes. Leaving those terminal records editable would let a
later change reuse that already accepted 3-to-4 increase, so version 4 would
not establish a stable compatibility baseline for persisted user data.
Capture the accepted root classifications and semantic record digests in a
separate finalization checkpoint. Validate that checkpoint before rendering
or writing acknowledgement artifacts, and preserve its accepted records even
after the current writer advances. Keep publication evidence independent:
the verified released version remains 3 until a release establishes otherwise.
Compare future writer-4 schemas with the accepted baseline using the existing
compatibility classifier. Compatible additions remain permitted through new
same-version acknowledgement records. Breaking changes require a higher
writer and their own header transition; comments, aliases and source-location
changes do not alter the accepted semantics. Later unaccepted compatible
records retain the existing update workflow.
Document the distinction between finalization, publication and compatible
evolution, with regression coverage for locked records, incomplete or
inconsistent checkpoints, additive changes and future-version transitions.
The master merge invalidated docs/subsystems/slots.md's recorded pairing hashes; the minimal-preset UI golden still expected the breadcrumb as a disabled button, which darwin drag-band avoidance intentionally turned into plain text; and the app-region nudge effect's unattached-ref return had no darwin first-render test.
Merge master 0e9385ad8b9e75146f4baec2e731dd961f11c87c, observed when
this final integration step started, into the merged V4 release branch.
Keep the complete upstream history and do not advance to later master
commits during finalization.
Bring the Web trust-domain and relative-route handling, plugin install
cancellation, and npm registry mirror configuration into the common
release baseline. Retain the V4 codecs, producer-source policy,
parent-owned catalogs, and fork/migration behavior from the merged stack.
The merge needs no manual conflict resolution and changes no Session
JSONL inputs, snapshot generations, format packages, or persistence
schema/history files. This checkpoint precedes the separate V4 freeze
and audit fixes so each change keeps its own rationale and validation.
Adds the implemented Agent Note for the rule issue #4573 exposed: an
out-of-tree plugin declares every host runtime whose cross-instance identity
matters as a peer (plus dev), never as a dependency.
Records the mechanism (dsh-scope mints its tag per module instance), what the
duplicate does to a per-Agent registration, the alternatives that lost
(mounting on agent.ctx, a shared Symbol.for tag, version alignment), and the
open coverage gap: verify-package-dependencies selects no packages/experimental
package, so tests/shared-host-runtimes.spec.ts is the only pin.
Master's sessionless-sidebar work renamed the conversation leading seat to
root-scoped conversation.header.leading instead of deleting it; the note now
states both seats' current roles rather than claiming outright removal.
The fixed fullscreen right panel sits over the frame's leading band and the
sidebar logo row's drag rects, so without a whole-box no-drag its
non-interactive content dragged the window; restore the subtraction (darwin).
The shell.leading note and AppFrame comment now state the real mount
condition (darwin && sidebarCollapsed) instead of the platform-agnostic
collapsedWidth === 0 claim, which Windows' caption collapse also satisfies.
Clear pointerFocus on pointerup/pointercancel so a press that never moves
focus cannot swallow the next keyboard focus; match prefixed/unprefixed
app-region with !important tolerance and prove the allowlist path rejects
an offender; scope the ui-layout shared-curve limitation to data-animating.
A profile installs @deepseek-ai/dsh-experimental-browser-use-runtime from npm
beside the dsh installation, and that package declared @deepseek-ai/dsh-scope
and @deepseek-ai/dsh-mcp-client as dependencies, so the install carried its own
copy of both.
dsh-scope mints its scope-tag symbol per module instance, so the copy's
createScope tags a context the host's scopeOf cannot read. Every Agent's MCP
tools then registered in the global tool layer: the first Agent succeeded and
each later Agent's tool synchronization hit a name conflict, which
failOnStartupError turned into a failed creation.
Both are now peer dependencies (plus dev), which is how every other consumer of
ctx.tools and dsh-scope declares them and how a profile install reaches the
installation's single instance through the profiles module fallback.
schemastery stays a dependency: it carries no instance-local state.
Fixes#4573
master's elevation contract (elevation-styles.client.spec.ts) rejects a
neutral border beside an lv/elevation shadow: the stroke belongs to the
shadow. The registry options keep their l3 hairline through
--dsw-elevation-stroke-color, as Menu does, with border: 0.
Windows directory junctions are reparse points that lstat reports as symlinks, so list refused My Music, My Pictures and My Videos with workspace-file/not-directory while its own listing reported them as directories. list now follows a final link, requires the resolved target to be a directory inside the workspace root, and keeps read's no-follow gate.
The cancelled-between-attempts case raced a zero-delay timer against the
file restore between two registries; under parallel load the restore won,
the second attempt started, and the once-only mock let it reach the real
pnpm. The stop now lands while the registries are read, before any run,
which is the same guard and cannot race.
The input-message definition classifies ordinary user, steering, and injected
context messages. Put that supported path first so readers can follow its
matching rules directly, then handle deferred developer history afterward.
Keep the append/checkpoint predicates, matched identity, null result for
other events, and explicit developer error unchanged. This is a local
readability change with no presentation or persistence behavior change.
Validation: all 61 conversation-node definition tests pass, including user
classification and developer refusal. Scoped lint and whitespace checks pass.
Session V4 reserves developer-role messages for incremental model-visible
session changes. The role, tool addition/removal blocks, and deferred-loading
marker need a durable representation before independent provider and UI
integrations can emit or consume them. Known developer events must match an
open turn and step; unsupported providers and UI consumers fail explicitly.
Request headers already capture full tool definitions. Persisting another
ToolSchema in each addition would duplicate Session information and create
two definitions that could drift. Additions therefore store toolName, and
the containing developer event uses headerSeq to select one earlier known
request/header for all its additions. A same-name replacement keeps its
historical definition identity through restart, fork and surface changes.
Removals require no definition copy, and ordinary developer messages omit
headerSeq. Generic sourceEventSeqs remains independent derivation metadata.
The rejected inline tool field is optional never with @persistenceReserved.
The schema extractor retains that uninhabited slot, so permitting a future
value requires a format bump instead of being mistaken for compatible
optional metadata. Unrelated optional fields remain additive and readable.
Native and Session admission reject missing, forward, non-header, unknown,
ambiguous and incomplete referenced definitions, and reject the retired
inline tool field. Validate complete schema fields only for referenced
definitions, preserving unrelated header behavior and extra JSON metadata.
Unknown ignorable records remain opaque. Empty developer nodes retain their
surface position without producing a message or shadowing the system head.
No automatic emitter, provider activation, UI implementation, or speculative
public resolver is added.
Keep the core-owned developer source binding beside the introduced role,
including the request-only type rejection and attribution round trips. Update
consumer guards, SDK/snapshot handling, bilingual owner documentation, and
the same unreleased V4 acknowledgement. Keep earlier tool/source migration
work and its documentation in the parent. Preserve the existing recorded
Cordis inspection generation from the original developer change.
Integration keeps the final source layer as the sole parent and preserves
master's Messages input filtering, empty-user omission, empty tool results,
and system-update placement. Developer/deferred guards still run before
conversion. Native V4 admission, header references, reservation declarations,
and generic tooling match the validated marker candidate. Parent-owned
fixture and generator fixes remain in the parent.
Earlier feature validation passed full build, full lint, 41 doc-sync gates,
and 16 hygiene gates. The unchanged native implementation retains that evidence.
The focused Session/V4/request-input run passes 884 tests across 38 files with
100% statement, branch, function and line coverage for the V3-to-V4 package
and Session surface. Consumer tests pass 482 with one existing skip; tool,
message and optional-field tests pass 146. Cordis inspection and compaction
replays pass in both source and built modes (two cases per mode). Ten invalid
referenced-definition/unknown-header controls failed before their scoped
guards and pass after them.
Reservation validation: the actual generated developer root records tool as
optional primitive never. Widening it requires a bump, while traceId remains
additive and preserved; all 85 focused reader/classifier tests pass.
Final-parent validation: 205 tests across eight provider, type, catalog,
Session and native-format files pass. The preserved parent fixture verifies
Messages input-history replay; runtime/declaration and parent-owned-file
comparisons show no loss during the final rebase.
Co-authored-by: Tianyi Cui <[email protected]>
The producer-source refactor introduced the unreleased V4 discriminator
code-mode even though this source identifies images deferred from successful
PTC subcalls. Name it ptc-mode before V4 ships so persisted attribution
matches the producer and does not freeze obsolete terminology into new logs.
Keep the live writer, V3-to-V4 migration output, collision guard, and reserved
producer policy aligned. Both released tools-code-mode and tools-ptc plugin
lookup keys map to ptc-mode. Preserve those historical keys, opaque message
text and IDs, all frozen V0-to-V3 generations, and recorded Session inputs.
Update the owning tests, producer documentation, current catalog, and V4
acknowledgement. The canonical schema audit identifies exactly three changed
roots; reversing only the source literal restores each prior digest. The
large generated JSON diff comes from graph ordering, with 61 roots and 572
types retained.
Validation: the seven-file focused run passed 298 tests and exposed four
stale current-output expectations in two migration files. After correcting
those expectations, both files passed all 23 tests. All 41 documentation
gates and full lint passed; frozen generations and recorded inputs are
unchanged.
Give durable user context its producer-owned source kind instead of a generic plugin wrapper. The V3-to-V4 conversion preserves the frozen rename and collision rules, all own JSON metadata, event coordinates, and historical request-only kinds. Native catalog and JSONL admission retain source validation so unknown attribution can survive without its producer while malformed owned sources are refused.
Use an explicit user-only RequestUserInput for auxiliary LLM prompts that do not own durable message identity. Auto Review and compaction remove their request-only source registrations while retaining prompt content and freezing; provider serializers and projections keep the same wire data, assistant replay isolation, image ordering, and tool correlation. Session writes, Agent delivery, and recorded title requests continue to require identified durable messages.
Bind the saved compatibility policy only to user sources and qualify tmux location attribution without weakening fixed system, model, or tool semantics. Rebuild the current inventory and unpublished V4 acknowledgement on the actual tool-role parent, preserving its rationale and all accepted historical records. The catalog format version records tooling metadata; Session remains V4. Developer-role policy and activation belong to their own feature.
The latest-master Messages adapter, serialization, request-input typing, EpochHeader reservation, and attribution checks pass 103 tests across five files. The retained V3 Messages input-history snapshot passes through the source profile; Host/Client typechecks and all 41 documentation gates pass. The generated comparison records four changed roots and 447 unchanged type fingerprints. Latest-master ARIA changes are retained, with producer source labels applied independently; new Messages fixtures retain their producer names as declared source kinds and use the fixed system-message helper; recorded Session generations are unchanged by this regrouping.
Co-authored-by: Tianyi Cui <[email protected]>
Native V4 admission and Session adoption reject every request/header.header.system field because system prompts belong to system/message events. EpochHeader omitted that key, so the generated schema classified adding optional system text as a same-version optional-property addition even though released readers would reject it.
Declare system?: never with the shared persistenceReserved marker so the schema records the existing prohibition. Widening the field to string now changes a forbidden value set and requires a version bump. Keep both runtime guards and all unknown-metadata handling unchanged, and document the reservation alongside the current header type and V4 acknowledgement.
The regression extracts the actual event:request/header schema, checks the optional-never field, widens only that property, and verifies the version-bump diagnostic and native refusal. It fails without marker extraction and passes with the shared tooling support. The focused reservation, retired-syntax, and Session surface suites pass 65 tests; all 436 type-equiv blocks match their source. Generic marker extraction belongs to the foundational tooling change.
Represent tool results as tool-role messages and migrate released wrappers
without losing outer own JSON fields, call identity, error state, or writer
field order. Refuse nested or wrapper-only information that has no lossless
current representation before successor publication.
Validate current event relationships directly instead of projecting V4 data
back into V3. Keep lifecycle, PTC, title/command, compaction, system-field and
retired-syntax checks on catalog restoration and ordinary JSONL opens. The
Worker JSON equality helper must inspect own keys so it cannot admit PTC
arguments that Node rejects.
Keep the source role change, required native admission and consumer fixture
updates in one complete review layer. Explain shared-type changes in the
persistence record and the native reader rationale in its existing note.
Use checked branded fixture identities and discriminant narrowing instead
of unknown double casts; fixture values and assertions remain intact.
This recomposes the original tool-role work by _Kerman with the independently
validated migration and admission repairs by Tianyi Cui. Frozen generations
and snapshot input version selection are retained.
Co-authored-by: Tianyi Cui <[email protected]>
A reader can reject a property name while its type declares that property
as optional never. Dropping it from the extracted schema makes a future
optional JSON value look additive, although the existing reader refuses it.
The type history must retain that explicit prohibition.
Support the argument-free @persistenceReserved marker only on optional
properties whose values are never or undefined. Keep those properties as
optional primitive-never nodes, so later JSON values change an existing
field and require a version bump. Reject invalid placement, arguments,
repeated tags, required or JSON-valued declarations, and mapped uses that
violate the reservation. Keep every unmarked omission rule unchanged.
The classification regression failed before this change with an additive
optional-property result and now requires a type-change bump. All fifteen
focused reservation cases and all 210 tooling tests pass, including inherited
fields, misuse, and unmarked preservation. Host typecheck, scoped lint, and
pairing pass. Regeneration leaves the unannotated baseline catalog and
inventory byte-identical; all 61 roots still match their existing history.
No runtime property is activated in this tooling commit.
Traversal ordinals and first-owner paths gave shared persistence types
unstable display names. Keep authored names and source locations as metadata,
use structural digests for identity, and render complete kind/form groups
with bounded array labels. Preserve historical rendering and stored graphs.
Record supported source compatibility promises with their property binding,
policy version, discriminator, preservation rule, and qualified kind set.
The extractor and classifier require explicit opt-ins in both compared
schemas; ordinary or unsupported changes retain strict classification.
Policy-bearing graphs use the format-2 fingerprint domain while unannotated
graphs keep their format-1 fingerprints. Runtime owners activate bindings
only with the corresponding reader behavior; this tooling layer activates
none and changes no package source or Session generation.
Provide a read-only before/after review command that groups shared structural
evidence without replacing authoritative compatibility diagnostics. Match
changed alternatives only through unique literal discriminators and retain
ambiguous additions and removals as independent facts.
At the X baseline, all 61 roots and 472 schemas, digests, and source fields
are unchanged. The inventory remains format 1 and removes only 296 synthetic
names. Existing acknowledgement history still matches every root. All 195
focused tooling tests, Host build/full lint, and bilingual pairing pass.
The merged performance/usage preference hides elapsed-time controls by
default. The release-only mid-turn fork scenario still waited for
"Ran for", so its ARIA check timed out despite a completed child turn.
Wait for the visible, enabled branch action belonging to live turn 2.
That action requires the settled turn tail and directly exercises the
fork affordance this scenario owns. Preserve the existing exact-cut,
synthetic closer, resume-header, completed-turn, and streaming checks.
Keep the default preference rather than enabling unrelated timing UI.
Refresh only the obsolete elapsed-time button in the ARIA expected
output. Session JSONL inputs, historical generations, snapshot versions,
and production code remain unchanged. The former wait failed locally;
the four-case scenario passes in ordinary comparison mode after this fix.
The control beside the guide, the attempt line, the run badges, and
the failure lists name a registry by its name alone; only the option
in the list carries the host it names.
The default registry reads as such with the host pnpm names, the
mirror carries its host too, and no line under them repeats it or
describes the order the Host would ask; the order shows only as the
Host asks it.
Arbitrary-sequence forks place the inherited end-seed marker before their
synthetic tool, step and turn closers. The Session constructor receives that
whole prepared seed, so firstLiveSeq can follow the inherited marker and the
constructor is no longer its sole producer. The retained documentation still
claimed both facts and could lead a consumer to use the wrong ownership cut.
Name buildForkSeed and the constructor as the two core producers, distinguish
the inherited cut from ordinary restore markers, and retain the warning about
plugins appending their own markers. Correct the subsystem reference's claim
that interrupted is the only synthetic turn ending: forked is also never
emitted by a live loop. Keep the bilingual reference and type-equivalent
excerpts aligned with the source JSDoc.
Refresh the required persistence catalog after the JSDoc change. Its schema
inventory changes only three source-line locations; all 61 roots, 472 types
and fingerprints remain identical. Parsed TypeScript declarations printed
without comments are also identical, so this changes neither runtime behavior
nor persisted fields, historical generations or recorded Session fixtures.
Merge master 01d96c168a84227374e3aa741e94ddfbb498e27e, observed when
this integration started, after the published 6de3db41 checkpoint. Keep
master's author history and preserve the release writer, adjacent
migrations, historical Session generations, and parent-owned catalogs.
Combine blank Session writer acquisition and superseded-startup
cancellation with the release branch's projection refresh and panel
preservation. A startup acquisition may finish after a later navigation,
but must not retain a stale main Session or reveal its Conversation.
Update the existing regression's retention expectation while preserving
its panel and active-navigation assertions.
Keep the release catalog loading, known-leaf, and parent availability
behavior alongside master's completed-turn status and aligned sidebar
icons. Regenerate the config locations and pair the combined README
contracts. Preserve upstream's Linux process-state assertion repair.
The plugin-config e2e pinned the old terminal one-liner and its golden still
carried the three pre-revision summaries; the ui-plugin-manager READMEs still
quoted the renamed install-guide button. Refresh the golden, update the
assertion and both READMEs, and re-record the translation pairing.
The Plugins settings inventory drew the Loader entry identity (cardIdentity)
at 11px in the code font, one step below the 12px the same identifier gets in
the expanded card's entry row. On Windows the code stack resolves to Consolas,
where 11px reads thin. Raise it to 12px/18px so it matches the sibling
identifier row and the page's other small text.
Fixes#4496
The Windows caption menu host sits at z-index 1100 in the strip that --dsh-windows-menu-start reserves to the right of the sidebar toggle and New Session, so the sidebar's right-side hover bubbles rendered underneath the Application/Edit text. Render those two bubbles below the 40px caption instead; ordinary Web and macOS keep the default right side.
With the guide open, unfolding the registry options pushed the dialog
past the viewport, and the card did not scroll. The options now float
from their toggle in a portal, right-aligned to it, folded by Escape
(claimed before the dialog's own Escape) or a pointer outside them,
and by the store when a run starts; useAnchoredPosition gains end
alignment for this. The dialog's content region scrolls under its
footer. The mirror reads as the mainland China mirror without a tag.
* fix(session): keep blank session discovery local to each host
* fix(session): acquire blank writers before reusing sessions
* refactor(workspace): separate saved selection restoration from blank lookup
* fix(workspace): create after the selected blank writer is held
* test(web): align blank reload snapshot with current icons
Review of #4539. A refusal pnpm view prints as JSON on stdout was read
as unknown, so no registry was ever asked after a dead one; stderr and
stdout are now read alike, and a lookup killed at its bound is a
network refusal. pnpm's own registry joins the fallback plan only once
pnpm config get registry names npm's own or a configured fallback, so
a company registry in .npmrc is never followed by a public one, and a
registry pnpm already names is asked once. The Host lays each failure
at the registry or the spec's own host, reading the host off error
lines only, and reports it as ChangeResult.failedAt; a stop that lands
between attempts is cancelled, not failed. The package publishes its
registry rule through ./registry so the dialog plans with the Host's
own function, starts from the registry the Host asks first until one
is remembered, tags that one as the default, labels pnpm's own by the
host it names, and words and offers a change of registry from the
Host's attribution. ERR_PNPM_NO_OFFLINE_META is no longer a network
failure.
The Windows coverage job can schedule the test after the old 200 ms
publication delay has elapsed. File existence then no longer proves an
empty lock, so the setup assertion fails even though the installer is
correct. The original implementation reproduces that failure when parent
inspection is held until the record has been published.
Replace the delay-only test hook with two opt-in fixture barriers. Hold the
publisher after exclusive creation, and acknowledge the contender only
after it has observed an incomplete record. The test checks the empty bytes
before releasing both processes and always releases and awaits them during
cleanup. Existing normal-path lock and initialization budgets are unchanged;
no test barrier is entered without its explicit environment setting.
The focused barrier case and all 38 installer tests pass, as do scoped lint
and whitespace checks. This changes test synchronization rather than
loosening lock ownership or increasing production timeouts.
Give every plugin artwork per-instance SVG def ids via useId so repeats on
one page keep their gradients and clips; drop the dead agent-team-web-profile
artwork row; keep the read-failure alert and retry visible on detail pages;
cover the new artwork components and the default-artwork branch; document the
artwork exports in the ui-primitives READMEs; refresh the plugin-manager and
plugin-install-cancel web expected outputs for the revised copy.
Master's agent-target update reports non-running children as inactive. The
release-only descriptorless-child regression still expected ready, so CI
failed before comparing the persisted result. Update only that status in
the assertion and its expected output; direct catalog discovery and the
absence of corrupt-child diagnostics remain asserted.
The expected log retains version 3 and every other byte. A built assembled
headless run passes in normal comparison mode, preserving the older input
format and its upgrade/reconstruction coverage rather than refreshing the
snapshot generation.
The current session's breadcrumb was a disabled button: the global darwin
no-drag rule subtracted it from the window drag band while its disabled state
and button user-select kept it from doing anything else, leaving the title a
dead zone that neither dragged the window nor selected. Plain text keeps the
native titlebar behavior — the title drags the window; ancestor crumbs stay
buttons.
Merge a2acc7d3eb620bb52f09f7ea3b5a3a1c23353e18 after preserving the earlier
master checkpoint and its successful CI. Carry current provider input-history
handling, UI controls, agent targeting, and runner-concurrency changes into
the integration base without changing the Session migration feature layers.
Keep durable direct-child catalog semantics alongside the upstream running /
inactive vocabulary. Resolve the snapshot description conflicts by retaining
that upstream status text and the existing descendants-only diagnostic rule;
the source and all 53 JSON tool descriptions agree. Preserve parent-session
availability handling with the upstream stale-blank and batched-history fixes.
The merged application builds, all 41 documentation gates pass, and focused
provider, tool-control, and Session Controller tests pass (98, 29, and 125).
Recorded input changes come from master: one refreshed v3 list-agents sample
and three new v3 scenarios; no migration bumps an existing snapshot generation.
The install dialog folds a registry control beside the guide: the
official registry, the mirrors the Host configured, and a typed
address, remembered in this browser and passed to the check and the
run. The installing screen names the registry the Host moved on to,
each pnpm run carries its registry, and a failed run names every
registry asked, or the host a git or tarball spec could not reach,
with a shortcut back to the registry options. The Host answers the
spec's own host and the registries it asked so the dialog can say so.
Also documents the registry plan in the package READMEs, the boot
subsystem page, the catalogs, and an Agent Note; pins the flow through
a real Host/browser e2e and refreshes the tool-schema snapshots.
Add registry and fallbackRegistries to the manager Config, ask them in
order from inspect and installBundle with --registry, and move on only
for failures another registry can change: unreachable, timed out, or a
copy the registry lacks. A git or tarball spec's own host is never
retried through a registry, and a registry outside the configured set
is asked alone. The lookup runs without pnpm's retries so a dead
registry is reported within inspectTimeoutMs; the install keeps pnpm's
defaults. Results carry the registries asked, the install-state event
names each attempt, and the plugin_manager tool takes a registry.
The seat note claimed a plugin-manager inline clearance formula that never
shipped (entry pages pad by the unconditional --dsh-frame-top-clearance) and
omitted the 84px fullscreen clearance tier. The vibrancy note still called the
sidebar top strip a drag region after that declaration moved to the frame's
.leadingBand, said drag lives in "exactly two places" while the Windows caption
::before is a third, and lacked the dsh-desktop:window-fullscreen relay that
drives html[data-fullscreen]. The ui-sidebar README carried the same stale
top-strip drag claim. Chinese sides follow; pairings re-recorded.
* fix(web): make the plan chip a business-accent control at row height
The chip borrowed the warn-state pair for a plain mode flag, so it read as
a warning, and its 2px vertical padding left it 4px shorter than the + and
select controls beside it. It now uses the business accent pair, takes the
row's 28px height, and carries Chinese copy; the unused chip.off.*
dictionary entries had no surface and are dropped.
* docs(web): re-record the plan chip README pair
* docs(web): state the plan chip's close glyph and drop stale copy claims
The READMEs described the chip without the × the component renders, and the
zh side still quoted the English accessibility string after this branch gave
the namespace its own Chinese one.
* test(web): pin the plan chip's accent tokens, height, and Chinese copy
The lifecycle-chrome lane asserted the chip's warn pair; it now asserts the
business pair plus the 28px row height, and a zh plan-active golden records
the 「计划」 label the chip shows under the Chinese locale.
* fix(web): color claimed command tokens with the business accent
The draft's claimed token still used the warn label color while the plan chip
moved to the business pair, so a claimed /计划 read orange above a blue chip.
The claim decoration and the tests asserting its inline style now use
state-business-primary.
* fix(web): keep the composer control row on one line at narrow widths
The fixed-width mode chips and send circle left the row's 12px gaps deciding
between a slack first line and a wrapped trailing group: beside the plan chip,
a long model name moved the model trigger and send circle to a second line
while the first line still had room. Narrow rows now use 8px gaps, which holds
the row together down to the icon-only model trigger.
* fix(web): drop the context ring in the narrow composer row
With plan mode on and a long model name, the 28px occupancy ring plus its gap
was what pushed the trailing group onto a second line. The ring yields at the
same row width that already hides the access-mode label, so the fixed controls
keep one line; the reading stays reachable from a wider window.
---------
Co-authored-by: Turtle <[email protected]>
subagent-activation-limit and plugin-manager-mcp landed after this branch
diverged and still pinned the idle/ready descriptions; regenerate both
through their owners so every pinned header carries the shipped text.
Sync the pinned schedule-catalog tool-schema sidecar with the refreshed
list_agents, send_message, subagent, and subagent_fork descriptions; the
catalog e2e does not compare it, so the refresh missed it. Rewrite the
Agent Teams note's Steer sentence and the ownership-release limitation so
`idle` no longer appears where the roster vocabulary is running/inactive,
and fix the two-clause comma splice and a zh full-width spacing slip.
Electron recollects window drag rects only when a style or layout pass
dirties an app-region value, and Blink skips hidden boxes when collecting
them, so the first open of the slid-off panel left the strip swallowed by
the drag band until an unrelated layout ran.
The release-only mid-turn fork scenario retained the old accessibility
children after master switched common icons to decorative presentation.
Refresh its expected tree from the built scenario: the same top-level
button names, failure text, parent/child prompts, and response remain.
Only redundant icon and label children disappear.
CI reproduced the mismatch. All four fork cases pass after refresh and
again in normal comparison mode. A mechanical comparison confirms that
removing nested children and their now-unused parent colons is the entire
expected-output change. The recorded Session generations and production
code are untouched.
* ci: trial smaller Blacksmith static and observational runners
* ci: measure the 16-vCPU control for runner sizing
* ci: use 4-vCPU static and 8-vCPU observational runners
* ci: retain 8-vCPU static runner for completion time
The latest master hides the catalog chevron from the accessibility tree.
The release-only catalog-push snapshot still expected that image child,
although the button retains its complete "2 subagents" accessible name.
Refresh that expected output after the actual built Web scenario; its
recorded Session input and all interaction assertions remain unchanged.
The old expectation failed against the merged application. The refresh
changed only this three-line snapshot, and the normal read-only rerun
passed all 11 subagent conversation cases with every replay call consumed.
Merge master 0fdadce5189dcf13a00c2ed8293be95b9bb29c2c into the reviewed
Session V4 integration base. Preserve its V4 persistence and direct-catalog
subagent navigation while carrying forward current UI controls, decorative
icons, Desktop signing/runtime changes, and upstream tests.
Resolve lineage conflicts by retaining interaction-time catalog authority,
known-leaf handling and row order together with the shared status icons.
Keep both Desktop shell-routing regressions and one production route; master
now includes the route repair previously carried by this integration branch.
The full build, 41 documentation gates, two Desktop shell tests and 34 focused
Client lineage tests pass. Further built Web and packaging checks precede
publication. Preserve this pinned merge checkpoint before taking a newer
master tip, and propagate it through the reorganized feature stack.
* ci: right-size Blacksmith runners by workload
* ci: retain static runner capacity supported by utilization
* ci: retain observational runner for peak CPU and memory demand
* ci: restore Windows build to 16 vCPU after runtime regression
* ci: trial 4-vCPU runtime packaging to reduce CPU time allocation
* ci: trial 2-vCPU Linux packaging and serial Windows tests
* ci: include Windows packaging in 2-vCPU trial
On darwin the expanded brand renders as a plain span instead of the New
Session shortcut button, so it stops matching the global interactive
no-drag rule and inherits the logo row's drag region; the dedicated New
Session button keeps the action.
While the Conversation is selected and its header shows the view tab strip,
the macOS window drag band grows from 52px to the 76px header block (title
row plus tab strip), so blank header space drags. The band matches the
strip's data-conversation-tabs marker via :has, so a compact header (single
view, per #4451's developer-tools setting) falls back to the 52px strip.
Keeps both window-chrome features: this branch's macOS shell.leading seat,
single drag owner, and fullscreen sync alongside upstream's Windows desktop
titlebar (#4407). ipc.ts and preload-app.ts merge both platforms' channels
and preload wiring; Tooltip merges upstream's suppression context with this
branch's pointer-focus suppression; slot-catalog regenerated.
One 52px leadingBand in the AppFrame (frame's first child) plus the sidebar
logoRow are the only -webkit-app-region drag surfaces; everything interactive
subtracts itself through two global base.css rules (interactive elements and
body > :not(#root) overlay portals), so header controls, breadcrumb chips, and
strip tabs stay clickable under the band. A ui-theme stylesheet gate pins the
two-file drag allowlist. The frame publishes --dsh-frame-top-clearance (48px)
for entry pages (plugin manager list and detail) beside the existing
--dsh-frame-leading-clearance, replacing per-page hardcoded insets. The
shell.leading window-chrome seat moves the collapsed-sidebar reopen controls
from the conversation header to the frame. Sidebar vibrancy tint drops the
blue cast in dark mode and halves its fill opacity, light mode softens the
blue mix, and the logo row tightens 4px above New Session.
The shell asset regression substitutes a real package path through getAppPath, but the shared test harness inferred only the literal desktop-test-app. Vitest executed the test successfully while the repository TypeScript build rejected the path override.
Declare the mock return type as string to match Electron getAppPath and permit both fixture and real package paths. This changes only test typing; it adds no casts or production behavior. The focused production-routing regression passes after the correction.
A child conversation rendered in a parent Sidebar keeps the child Session identity for reading its plan. Routing navigation through openResourceIn(child) confused that document identity with the layout owner: a child with no adopted Sidebar store silently opened nothing, and an existing child store was invisible.
Use the mounted Sidebar navigation service for both logged and temporary plan previews. Keep the complete child Session resource address, so reads and reload still target the correct document. A real-controller regression adopts only the visible parent store and proves both child entry points populate it without creating child layout state; it fails before the fix.
All 47 ui-plan owner tests and all four built Web plan-review cases pass, along with the full build, scoped typed lint, and bilingual pairing. No Session event, stored document, fixture generation, or resource-address format changed.
The imported conversation note names a chat resource host that the current parser rejects. The provider is registered under subagentchat and its existing helpers and reference documentation use that host. Correct both language examples without changing addresses, parsing, or persisted layouts.
The workspace-update qualification runner still copied lib/preload.cjs after the standalone management preload and its build entry were removed. A clean Desktop build emits only the application, mandatory-update, and update-dialog preloads, so qualification failed with ENOENT before Electron could start; stale outputs could conceal that failure.
Remove only the obsolete copy-list entry. Keep all three current preloads required and retain ordinary copy failures. Executing the actual copy loop against artifacts from a fresh isolated Desktop build now copies exactly those three files byte-for-byte while lib/preload.cjs is absent; the same loop failed at that missing file before this change.
Ordinary and mandatory update windows still load dsh-app://shell documents after the standalone plugin manager was removed. The production protocol handler handled only the app hostname, so their existing HTML, scripts, and styles returned 404; mandatory-load failure deliberately retains the modal block.
Route the shell hostname to the packaged renderer directory through the existing static-asset helper. Retain its GET/HEAD, path-containment, MIME, and missing-file rules; keep isolated preloads and owned-window IPC authorization unchanged.
The regression imports the real main entry, captures its registered protocol callback, and reads all seven actual update assets through the real static helper. It fails with 404 before the fix and passes afterward, including HEAD, rejected methods, malformed paths, traversal, and unknown resources. Sixteen adjacent static/dialog/overlay tests pass, and the Desktop package builds from a fresh worktree.
The expected-output CI lane sets DSH_EXAMPLE_MODE=lib. Selecting tsx/esm
alone did not exercise the source bin there, and the compatibility test
only checked --help, so removing source profile link resolution could
silently restore duplicate source/lib Tools instances in a green CI run.
Run the existing real headless-profile tool round trip explicitly in both
src and lib modes. The explicit mode overrides the CI environment default;
the source case uses the supported tsx/esm entry. Both cases retain the
real profile, shell tool, persisted Session assertions, stdout, and stderr
expectations, with isolated temporary directories. No recorded generation
or output normalization changes are needed.
After a full build, both cases pass with DSH_EXAMPLE_MODE=lib. Negative
control: removing only the source bin link-resolution selection makes the
source case fail at scheduler.prepare while the built case still passes.
Restoring the implementation makes both cases pass again. Scoped lint
passes. This puts the regression in the existing post-build expected-output
CI lane without adding artifact requirements to compatibility-only jobs.
The success fixture can run under either tsx hook, so its previous output
assertions stayed green when runLoaderSmoke stopped forwarding sourceImport.
Line coverage therefore did not protect the supported ESM-only launcher.
Include the actual child process execArgv in the fixture output and assert
the resolved tsx/esm hook requested by the caller. The regression observes
Node launch arguments rather than reproducing the helper implementation.
Align the optional sourceImport spread with the adjacent mode spread;
this keeps the same behavior for defined and absent values.
Negative control: removing forwarding passed the old test but failed the
new assertion because the child loaded tsx/dist/loader.mjs instead of
esm/index.mjs. With forwarding restored, all 25 loader-smoke tests pass
and src/index.ts retains 100% statement, branch, function, and line coverage.
Scoped lint passes.
The shared Subagent card submits the model-selection and limits namespaces
independently. The model-selection write can finish first, so its presence
in settings.yaml does not establish that maxDepth has reached disk. CI
observed subagent: {} with the completed model allowlist at that point.
Move the existing exact-label disabled Save wait before the persistence
assertions. The form uses a distinct Saving label until every namespace
save finishes, so this observes the shared operation's completion before
checking both results. Retain every assertion, timeout, and expected file.
A diagnostic barrier on the real subagent settings mutation reproduced the
original missing-maxDepth failure with both correct requests recorded and
the form still saving. With the relocated wait, releasing that same write
persisted maxDepth without another user action. The barrier run passed all
10 cases, and the unchanged whole plugin-config Web suite also passed 10/10.
The runtime profile default is correct for installed applications, but the
supported TypeScript CLI also runs tsx workspace aliases. In that launch,
profile resolution loaded both packages/core/tools/src/index.ts and
packages/core/tools/lib/index.js. The ToolRuntime instance and agent-loop
consumer therefore held different private TOOL_RUNTIME_SCHEDULER symbols;
the first tool call failed while reading scheduler.prepare.
The source bin now explicitly selects linked profile resolution using its
own .ts module identity. Built and packaged launches keep runtime resolution,
and explicit runProfile overrides remain unchanged. No service or symbol
identity is widened to conceal duplicate module instances.
Forward the existing sourceImport option through runLoaderSmoke and select
tsx/esm for headless source snapshots. This exercises the actual supported
source-launch vector while retaining the same recorded Session and expected
outputs. The loader-smoke subprocess test covers forwarding that hook.
Evidence: the official tsx/esm compaction replay failed before the fix while
loading both Tools module URLs. After the fix it loads only src/index.ts and
prints COMPACTION RECOVERED. The same focused replay passes under both
DSH_EXAMPLE_MODE=src and lib after rebuilding Host artifacts. The 43 CLI and
loader tests passed, including runtime defaults and explicit link/dual/runtime
overrides. Loader-smoke coverage passed 25 tests with 100% statements,
branches, functions, and lines. Host build, full lint, and all 41 doc-sync
gates passed. Recorded JSONL generations and snapshot normalization are
unchanged; temporary diagnostics were removed from built artifacts.
The pinned master merge updates the DSH package family to 0.1.6-alpha.2, but the release-only V3-to-V4 migration package has no master counterpart to carry that manifest change. Match the family package version so installed artifacts and hygiene describe one release identity. Session format remains V4; migration schemas, committed generations, and snapshot input versions do not change.
The combined source passes the full build, 93 Client tests, 631 persistence tests with one skip, all 41 documentation gates, all 16 hygiene gates, and full lint. The original pinned-master merge checkpoint remains in history.
Merge master 62b700bba7bb71e66d2d5a3b850ba8f4dd0fd6b4 into release/session-log-v4 at 4825bc01a999f564e79172a6af28e34fc757c540. Preserve this pinned integration checkpoint before any later upstream movement so the dependent session-format repairs have an immutable rebase base.
Keep release projection-backed child catalogs, independent Session references, exact fork behavior, V4 writer, and existing migration data. Adapt the incoming subagent sidebar chat to refreshProjections rather than the removed dedicated catalog API, preserve title projections in complete projection fixtures, and retain both sidebar actions and release lineage/status behavior. Remove the duplicate SessionProjectionMap import introduced by automatic merging.
Preserve both disjoint archive seals without changing any existing hash. Regenerate current Cordis and documentation catalogs from the combined source, retaining release session methods plus master slot factories. The frozen V0-V3 runtime paths are unchanged. Initial validation passed the full build, 93 focused Client tests, and all 1905 archived artifact seals; remaining integration and snapshot checks follow on this checkpoint.
The gate spec asserts line and rule per violation instead of counts; the
open-in-app fake carriers match the relative route exactly so a re-absolutized
route cannot pass; the root and mount cases of the markdown file route share
one table; a fixture echo of its own icon URL is dropped.
Any request outside the mount is an escape, not only same-origin ones; the
socket presence check is superseded by the frame polls; the redirect landing
is asserted by pathname. The scaffold's unused mount host option, hostFetch
mount routing, and the proxy's bare-mount redirect had no caller.
The maintained docs now name the four companion packages where they
still credited `ui-settings-plugins`: the web-app row comment, the
Plugins page README, the ui-settings README, and the client group
README, which gains a row per companion. The ui-settings README states
the `whileServed` lifetime and disposer ownership beside `bind`, and the
JSDoc says the caller owns the disposer. The settings-page cookbook
keeps the one home of the lazy-CJS build limitation for packages outside
this repository, its JSX example is a tsx fence, and the architecture
index and website label follow the page's title. The Agent Note records
the shell page's `plugins.item` id, the two companions that use
`ctx.remote` list api-remotes in `dsh.client.inject`, and the moved form
model test speaks of the form.
Move `load_workspace_dependencies` and the primary-runtime payload logic out
of `apps/desktop-host` into `@deepseek-ai/dsh-tool-workspace-dependencies`
so profiles other than Desktop can mount the same tool. Desktop keeps its
behavior through the package.
Two additions for carriers that are not Electron applications:
- `root` is optional: without it the payload is validated and used in place
(no copy under the Harness home), which suits read-only container layers.
- `runtime.json` accepts `linux`, and the `node` / `pnpm` components are
optional; the returned paths and the tool output omit what the payload
does not ship.
The `sdk` profile mounts the tool and `@deepseek-ai/dsh-skill-office` when
`DSH_PRIMARY_RUNTIME` names a payload directory (its sibling `office-skills/`
carries the skill resources); both rows stay disabled otherwise. Both
packages join the Python runtime closure.
Each official settings page now lives in a client package of its own:
`ui-settings-shell`, `ui-settings-agent-loop`, `ui-settings-subagent`
and `ui-settings-web-search` register into `plugins.item` while the Host
serves their namespaces, through the new `settingsScope.whileServed`
watch, each with its own dictionary and styles. The form model, the
fields and the form move to `ui-primitives` as the shared settings kit,
so a community package's browser half builds a page the same way.
`ui-settings-plugins` keeps only the Built-in plugins section shell
around the inventory tab.
The companions hold Loader rows in `web-app` behind the section; the
client catalog, config catalog and module graph regenerate; the
settings-card cookbook walks through a companion package.
The Plugins page declares three list slots as children of its main
entry: `plugins.detail.actions` renders at the head of a detail page
before its switch and uninstall controls, `plugins.detail.badge` after
the tags beside the title, and `plugins.detail.section` below the page's
own content. Every entry receives a `subject` owner prop naming what the
open page is about — a bundle, one row of a bundle, or an official item —
so one entry serves any page and renders nothing elsewhere. A row's page
still opens only for rows with a `plugins.row.config` entry.
The `plugin-config` web lane drives the three slots through the live
fixture on its bundle page and its row page, records a new bundle
golden, and shows the fixture contributing nothing to an official page.
* feat(web): reveal a clipped sidebar session title on hover
A session title wider than its sidebar row is clipped with an ellipsis, so a
long parent title hides the `(1)` that distinguishes its fork. Hovering the row
now scrolls the title to its far edge and returns it to the start when the
pointer leaves. The title stays an `overflow: hidden` scrollport, so nothing
reflows and no scrollbar appears; `scroll-behavior: smooth` supplies the motion
(reduced motion keeps the jump), and the hovered row clips instead of
ellipsizing because a scrolled box still paints the ellipsis over the revealed
characters.
Covered by the row component spec, the stylesheet contract spec, and a new
keyless Chromium scenario that hovers a seeded clipped row in the assembled
application and asserts the reached scroll extent.
* fix(web): return the revealed session title in one step
Review follow-up on the hover reveal:
- Return the title to its start with `scrollTo({ behavior: 'instant' })` instead
of a smooth scroll. A smooth return carried the resting ellipsis and the
narrowing cell over the travelling text for its whole duration.
- Scope `scroll-behavior: smooth` to `.sessionRow .title` so Workspace rows are
untouched, and fold the reduced-motion reset into the file's existing block.
- Guard the hover-only `text-overflow: clip` with `@media (hover: hover)`: a
touch tap latches `:hover` after the title has already returned, which would
otherwise strand that row without its ellipsis.
- Pin the contract in the browser scenario: computed `scroll-behavior` is
`smooth` by default and `auto` under reduced motion, and the return lands
inside one step. Add the unit case for the explicit instant return.
A collapsed plugin card carried both an enablement tag and a green root-fiber
status dot; for the common `active` phase the two stated the same settled
state. The dot now renders only for `pending`, `loading` and `unloading`,
the phases the enablement tag cannot state, and the expanded card keeps the
phase fact.
Related to #4424
The README enumerated every menu item, accelerator, and label behavior;
state only the capability and its platform condition. The Agent Note now
records the verified Electron mechanism instead of a bare claim: role
labels are English literals in Electron, re-applied to the native items
before the menu opens.
While the Host runs an install, the dialog's close control was disabled. It now asks the Host to stop the run, the same request as Cancel install, and closes the dialog once the Host confirms the stop; when the Host answers too late or cannot confirm, the dialog stays with that word. Escape and the mask keep waiting while the Host owns the operation. The control is labelled 取消安装并关闭 / Cancel install and close while it does this.
The cards and the pages of the Plugins page repeated their head (icon, opening title, tags, one-liner) and their top (crumb, icon, actions); CardHead and DetailTop carry them once, which is what the duplication gate asked for. DOM and behavior are unchanged.
- A bundle row with a configuration page is opened from its name, which carries an inline chevron like the plugin cards; the separate configure button and its `configure` label are gone. Status and switch stay at the right.
- The three install-guide examples carry an "示例:" / "Example: " label before the code.
- README and Agent Note record that a bundle's pages live with the row that carries its browser half: `dsh-client-modules` attaches that half to the row named by the bare package name, so a page keyed to a subpath row disappears with the root row.
Add the standard application hide commands (Cmd+H, Option+Cmd+H, Show All) and
pin them with the window commands in the startup test, drop the redundant mock
cast that failed the lint gate, move the Agent Note to bug-fix, and state the
English role labels and the Dock-activation condition in both READMEs.
The custom application menu replaces Electron's default one, so macOS lost
File > Close Window (Cmd+W) and Window > Minimize (Cmd+M) while the traffic
light and the Dock activation path still closed and reopened the window.
Declare the fileMenu and windowMenu roles on macOS, keep Windows and Linux on
the application and Edit menus, and pin the per-platform template.
The Plugins page declares three slots as children of its main entry:
`plugins.item` lists an official plugin's page in the Official group,
`plugins.bundle.config` renders a bundle's own form on its page, and
`plugins.row.config` gives a row a configure control opening the row's
page. Every entry renders in the `summary` and `page` views the page
passes as owner props; the page draws the title, icon, and crumb and
projects the ledgers beside its store, naming no configurable plugin.
`ui-settings-plugins` registers its four host-plane pages into
`plugins.item` while the Host serves their namespaces and withdraws them
when it stops; the forms keep edits until a save and drop them on leaving
the page, so the discard control and the unsaved marker are gone. The
Settings section becomes the Built-in plugins shell around the inventory
tab, and `settings.plugin.item` is retired.
The Official group opens with the optional bundles the installation
ships, tagged Beta where the feature is one and without an official tag,
followed by the configuration pages. Auto review leaves the optional
bundles and the CLI's dependencies; the install guide names it as its
example spec. The version tag is a capsule like the other tags.
The `plugin-config` web lane drives the pages through the real wire and
opens a community fixture's row page after switching its bundle on; the
settings lanes re-record the renamed navigation entry.
The document preview declares two child slots handing the previewed file
and its Host path to other plugins: the header actions after its own
controls, and the empty state of a file it cannot render, where Retry stood.
Empty-state read failures now name their recourse: not-text and too-large
offer the handoff, not-found and non-regular paths offer nothing, and the
rest keep Retry. The unsupported-suffix empty state offers the same seat.
ui-open-in-app fills both seats with an "Open" split button (default
application, or the file manager reveal from its menu) and an empty-state
"Open in default app" button. Both call the Session Remote the Host already
publishes: canOpenWorkspacePath once per page decides whether they render,
openWorkspacePath runs each gesture. Only the pressed control disables while
its gesture settles; a failure announces once through a transient toast.
The delivery cards keep their own opener this iteration. The design, slot
pair, failure classification, and toast rule come from Yifffan's PR #4120.
The mapping holds forty host suffixes, not thirty-eight: WeChat and QQ
each keep their own suffix, and Telegram and WhatsApp each add a second
short-link host.
Review asked for the sites a general audience visits. The vocabulary
grows from fourteen developer sites to thirty-four marks across search
(Google, Baidu, DuckDuckGo), video and audio (TikTok, Netflix, Spotify),
social and messaging (Facebook, Instagram, Reddit, Telegram, WhatsApp,
WeChat, QQ, Weibo), shopping (Taobao, AliExpress, eBay), reference and
community (Quora, V2EX), and Apple.
Matching now takes the longest host suffix, so weixin.qq.com keeps the
WeChat mark while qq.com keeps QQ. The spec covers one URL per site,
still requires distinct marks, and pins the Telegram and WhatsApp
aliases as well; the README and Agent Note record the categories, the
bounded per-site cost, and the licensing and trademark position.
Each unwrapped split column is its own scroll container, and only the
horizontal offset was mirrored onto the other side, so wheel-scrolling one
column left the other behind and the paired rows drifted apart. Mirror the
vertical offset the same way.
Review asked for a maintained artwork source instead of the fourteen
copied Simple Icons paths. SiteGlyph now imports siGithub and its
thirteen peers from the simple-icons package (CC0-1.0), added as a
ui-primitives devDependency and pinned by the lockfile, so a mark is one
host-map entry and updates arrive with the package. Each mark draws in a
28-unit viewBox inset by two units, which keeps the ~8% margin the
20-unit ic_ds_* glyphs carry, and the gallery fixture uses a synthetic
GitHub URL because verify-repository-references rejects the
organization URL.
Also from that review: the spec covers github.io and x.com, keeps its
comment truthful about which aliases collapse, and names the
single-label-host fallback case; the older clickable-link note states
the url glyph and per-site marks in the present tense; the package
README records that the known-site list is fixed and everything else
keeps the globe.
Regenerate the client and persistence catalogs, document the review address builder with a plain parameter, share the changed-file coordinate parsing between the comparison and native-open routes, and fold the summary and comparison stores onto one Host read store. A path with no existing ancestor but the filesystem root keeps its spelling on every platform, and the temp-root test redirects TMP and TEMP as well as TMPDIR while counting only the recorder's own directories.
Clickable external links now lead with the destination site's own mark
instead of the globe when the host belongs to a known site: GitHub,
GitLab, npm, PyPI, Stack Overflow, MDN, Wikipedia, Hacker News, YouTube,
X, Bilibili, Zhihu, Juejin, and CSDN. LinkIcon takes an optional href and
resolves the mark through a private SiteGlyph module; markdown anchors,
reference links, URL-promoted inline code, and web-card source and fetch
links pass their sanitized destination. Unmapped hosts, non-http schemes,
and the file categories keep today's glyph.
The marks are the single-path form of Simple Icons (CC0-1.0) on its
24x24 viewBox, filled with currentColor like every other link glyph, so
they follow the link alias in both themes and on hover.
Coverage: the LinkIcon spec pins one host per mapped site plus the alias
and fallback branches, the markdown and web-card specs pin their consumer
wiring, and the clickable-links gallery e2e pins the assembled mark
against the globe in a real browser.
The card header opens the turn's review on its first file instead of the common folder, so the folder route and its Host lookup go away and the native open lives only in the review tab. The split view without wrapping draws each side as its own column that scrolls sideways, so a long line no longer runs under the other side; with wrapping both sides still share one row per pair.
The login window navigated straight to the policy origin, so a slow or redirecting remote page left the window blank. The packaged placeholder is now the window's first document and the login request starts only after it settles; the first committed remote document replaces it, so nothing overlays the third-party page and a pending subresource cannot extend it.
The available state now reads "新版本" / "Update" instead of a sentence, and the downloaded confirmation keeps only "The update has downloaded." The entry also takes back the row's 2px bleed plus the foot rows' 2px inset so it ends on the same trailing edge as the other sidebar rows. The desktop dictionary additionally carries the login loading string used by the following commit.
The collapsed rail's update badge renders inside the expand button, so hovering it showed both the version notice and "Open sidebar". Tooltip now publishes a suppression setter that a nested tooltip claims while its own bubble is visible; the enclosing tooltip keeps its hover state and measured position, so its bubble returns as soon as the nested one is gone.
The per-file comparison tab becomes a `changes-review` tab per turn: a file selector in the header lists every recorded file, a card row opens the tab on its file through a navigation parameter, and the header's tools switch between the unified and a side-by-side view, switch line wrapping, open the whole file in the Sidebar, and open it in the default application when a desktop exists. View and wrap choices are kept per tab in an exclusive store.
Two oversized captures are no longer treated as unchanged, so the file is listed as oversized instead of vanishing; an oversized side outranks a binary one in the card and the tab alike. Captures read at most maxFileBytes + 1 bytes through a file handle, a missing file resolves through its nearest existing ancestor so a path created through a directory symlink keeps one canonical spelling, and ls-tree matches paths literally. The comparison tab draws at most 5,000 lines. Docs record that comparisons serve whole file text beyond the workspace root, the newline normalization, and the coarse payload bound; the card note's row behavior is current again.
The case verifies first-boot catalog browsing and an actionable
MISSING_CREDENTIAL, so it follows the default catalog instead of an
unlisted id whose route only proves pass-through.
The shipped DeepSeek catalog keeps V41 Flash and V4 Pro only; the removed
ids still pass through as text-only routes when configured explicitly.
Updates the adapter tests, the Web settings goldens that render the default
catalog, and the regenerated config catalog.
`dsh.title` was a manifest field this branch introduced: the manager read
it for `listBundles` and `inspect`, and the Web page showed it over the
package name. A bundle is named by its package name now, so `DshManifest`
keeps only its shipped fields, the page derives every heading from the
name, and the install dialog's subject line reads the name the Host
answered. The two e2e fixtures drop their titles and the manager lane's
goldens follow.
@deepseek-ai/dsh-experimental-client-ui-agent-team listed react under
dependencies, which the shipped client packages never do: react reaches the
browser through the web build. Now that the agent team web profile is an
optional bundle of @deepseek-ai/dsh, that dependency pulled react into the
npm install layout, where verify-npm-install-layout rejects it.
scaffold.ts required the launcher's built module at import time, so the
unit lane, which runs without a build, failed to load
scaffold-generation.spec.ts for its fixture-name helpers. The built copy
is now resolved on the first launch, which needs the build anyway; the
mounted copy is unchanged.
The info ring and the plugin pinwheel joined the hand-authored product
glyphs without the icon-set inventory following: master's count of 79
stayed after the merge while the barrel exports 81.
The Client entry re-exported ConfirmState, InstallState, ManagerNotice and
PluginManagerState from the controller store although no other package names
them; the page, its tests and the presentation module import them from the
store directly. Only the face, the page props and the locale key stay public.
The dsh manifest gains no description field: a bundle's card shows the
package.json description, and the shipped optional bundles carry no
display title or description of their own until localized metadata is
designed. The plugin author tutorial shows the bundle declaration alone.
OPTIONAL_BUNDLES in app-boot lists the bundles the installation ships
switched off; the plugin manager, the default-product isolation gate,
the workspace constraints, and the packed-install check read that one
list instead of a dsh.optionalBundles field in apps/cli/package.json.
app-boot no longer emits profile/reconciled after a reload, and the plugin
manager no longer turns such a generation into a plugin-manager/changed
notice: the Web page learns of a CLI or hand edit on its next read. The
change event keeps only the reasons a manager operation produces.
The retry that allows blocked install scripts drops the failed run from
the dialog instead of stacking the new run under it. The dialog no longer
grows past the viewport: the card is capped and everything under the
head scrolls, so the close and edit controls stay reachable however long
pnpm's output is.
reconcileProfilePatches finds the root Include only through the registry
the module that mounted it keeps, as it did before the Web page: a tree
mounted by another copy of app-boot was a test-harness situation, not a
product one. The web e2e scaffold now mounts and resolves the profile
through the built app-boot, loaded with Node's own require so it shares
the module map with the Host plugins the profile loads, instead of the
source copy a bare import resolves to.
When pnpm leaves a dependency's install scripts undecided, the failed
screen lists the pending packages and offers to allow them and retry in
place of plain retry; the Host saves the permission in the profile before
running pnpm again, and the installed screen names what was allowed. A
failure the Host could not name falls back to the manual instruction, and
a stale approval is worded like the other refusals. A new web lane drives
the whole flow through a fake pnpm that blocks, then installs.
The installation lists @deepseek-ai/dsh-experimental-auto-review beside
the two Agent Teams layers under dsh.optionalBundles, so a blank Web
profile offers it in the built-in group switched off. The bundle gets a
display title and one-liner, and its README says where it is switched on.
The dsh installation lists under `dsh.optionalBundles` the bundles it
ships for a person to switch on. The plugin manager reports them as
`optional`, and the Web plugin page shows them first, in a built-in
group with an official tag, off until selected and never removable.
Agent Teams ships this way, so a blank profile offers both of its
layers. Default-product isolation, the workspace constraints, and the
packed-install check accept that one dependency edge and nothing else
experimental.
A bundle's title and one-liner come from `dsh.title` and the new
`dsh.description`, else from the package name and `description`. A
dependency without a bundle patch is no longer listed unless the
profile selects it. The install dialog asks for a package name or
address; a guide under the field shows the three accepted forms with
an example each and ends in a security warning, and the copy no longer
speaks of a plugin ID.
The composer's paste path used RangeSelection.insertText, which keeps every
pasted newline inside one text node. In that document the browser reports the
collapsed caret's geometry as the whole block's, so deleting the draft's last
line moved the capped scrollport to the draft's head while the caret stayed at
its end.
RangeSelection.insertRawText splits the input into text and line-break nodes,
the document a typed draft already has.
Drop the "built-in components it changes" section from a bundle's page:
the rows a patch overrides are a composition detail nobody acts on from
the page. A failed enable, disable, uninstall, or row switch now says
which it was, and the Chinese toasts lose their trailing period. The
install dialog is worded around the plugin ID, which is the package
name, with a hint on what an ID looks like and no real plugin as the
example; the spec forms it names are the ones the Host's inspect takes.
A bundle's title is its `dsh.title` or a shortened name, so the page
never said which package a bundle is, and nothing on it could be typed
into an install elsewhere. The name now sits under the title as the exact
package spec.
Rows of the changed-files card open a `changes-diff` right-Sidebar tab that compares the file at turn start and turn end. The workspace-changes recorder copies every file a file tool edits into the Session's temporary directory before the first edit and again at turn end, so files git does not cover are compared without git and counted first-to-last; `maxFileBytes` caps copies and snapshot reads, and `diffTimeoutMs` degrades a slow line comparison to whole-file replacement.
Ported from fix/plugin-manager-ui-polish (cce193cff3, 4ed0f1c9cb) onto
the page as it stands after the #4182 re-port: a 32px rhythm on the page
and the detail sections; a four-arc pinwheel icon on the cards, the
detail head, and the sidebar entry; the version as a tag beside the
detail title and the crumb leading back only; a red-tinted uninstall pill
with its trash icon and a plus icon on Add plugin; a round refresh action
and caption-toned counts; 12px hover rounding across the rail controls;
and the group copy renamed to Installed. The non-bundle group and its
info tooltip are not carried, since the page lists bundles only.
The Host lists selected bundles before the rest of the profile's
dependencies, so switching a bundle moved its card to the front or the
back of the sidebar page. The store now orders the views by the title a
person reads, and a card stays where it is whatever the Host answers.
A repository whose baseline snapshot failed records nothing instead of
degrading to a file-tool summary; the write tool persists whether it
created or updated, so an unchanged overwrite outside snapshot coverage
no longer counts its content as added; hunks inside nested repositories
are dropped before check-ignore, which refused them with exit 128 and
lost the whole turn; a temporary root inside the work tree keeps the
recorder's own directory out of the snapshots; numstat keeps tabs in
file names; the summary carries line totals over the uncapped list and
the snapshot tree ids; the client's summary store abandons reads that
began before a connection reset. Docs and fixtures follow.
Master now carries the final #4182 plugin manager and the live client
module graph of #4189, so the web management layer is re-ported onto
them:
- The manager keeps master's `ManagementError` codes, `ChangeResult`
stages, `configure()` under `hmr.runExclusive`, `reload()` warnings
and `writePluginEnabled` name matching, and adds back `inspect`,
request-id installs with `plugin-manager/install-log` and
`plugin-manager/install-state`, `cancelInstall`, `listBundles` titles,
rows and overrides, `packageResult.kind`, `pnpmCommand` and
`inspectTimeoutMs`, and `plugin-manager/changed` after each change and
each `profile/reconciled`. A failed or cancelled installation restores
the snapshotted manifest and lockfile instead of master's one-shot
`pnpm remove` cleanup, so `cleanup` and `remainingDependencies` leave
the result.
- App boot keeps master's reconciliation diagnostics and emits
`profile/reconciled`; `watchUserPatches` goes with the launcher's
watcher now that HMR owns profile reloads.
- The settings Plugin list tab stays read-only with #4189's page-local
sync failures and retry; master's bundle form and row checkboxes are
dropped again. The sidebar page words the Host's codes through its
dictionary, and a client bundle change follows the live module graph
without a page reload.
- The web e2e scaffold no longer chooses a reload policy: the base
bundle's `hmr` row turns on with the profile context and application
readiness, and `profile.hmr: false` disables it through an overlay for
the startup-only scenario.
The sidebar Plugins page now drives `ctx.pluginManager` from #4182 and the
settings Plugin list tab goes back to read-only: its bundle form and row
checkboxes are removed, and the page joins `listBundles` with
`listPlugins` into one view per bundle, decides availability from the
inventory's `managementAvailable`, switches bundles and rows through
`setBundleEnabled` and `setPluginEnabled`, and confirms uninstalls.
The manager gains what the guided install dialog needs: `inspect(spec)`
reads a spec through the registry or a directory before anything installs
and refuses specs without a bundle; `installBundle` takes a request id,
streams each pnpm run as `plugin-manager/install-log`, reports its phases
as `plugin-manager/install-state`, classifies a failed run into
`packageResult.kind`, names the bundle it added, and restores
`package.json` and `pnpm-lock.yaml` when the run fails, is cancelled, or
adds a package without a bundle patch; `cancelInstall(requestId)` answers
only after pnpm exited and the files are back; `listBundles` carries
titles, one-liners, declared rows with their live entries, and the
built-in rows a bundle overrides; every completed operation and every
patch generation applied outside the manager emits
`plugin-manager/changed`. The config gains `pnpmCommand` and
`inspectTimeoutMs`. App boot emits `profile/reconciled` after each applied
patch generation and finds a root Include mounted under its pinned id by
another copy of the module.
Row diagnostics, dependency checks, and the non-bundle dependency group
are not carried over; the web e2e scaffold mounts a launcher-style
managed profile instead of the removed profile runtime.
The web plugin management layer now sits on the current-profile plugin
manager from #4182 instead of the boot and manager layers of #3593 and
#3594. This merge takes #4182's host side whole — `packages/boot/hmr`,
its `packages/boot/plugin-manager`, `profileContext` in app-boot, the
vendored loader change — and drops the replaced host packages and their
app-boot, inventory, Remote, CLI, and catalog edits. The web files of
#3596 are carried over unchanged: the sidebar page package, the
ui-primitives terminal changes, the settings-section tweaks, the e2e
scenarios and fixtures, and the two web Agent Notes. They do not build
against the new host until the following commit adapts them.
The file tools persist both sides of a hunk without a trailing newline,
so re-diffing an append read the previous last line as rewritten and
reported +2 -1 for +1 -0. Both sides are line-terminated before the
diff, the rule the per-call diff card already applies.
A working directory outside any repository, or a Host without git, no
longer loses the card: the recorder skips the snapshots and builds the
summary from the hunks the file tools persisted, with the working
directory as the workspace, so shell edits are the only gap. The
sidebar e2e scopes its chip clicks to the prose mention now that the
card offers the same open, and the present-svg and permission-policy
scenarios gain the card their write calls earn.
Opening the trigger menu from the launcher button has to put the keyboard in
the editor for the arrows to drive it, and Lexical's focus restores its
selection a tick later — a track the controller read as an empty draft and
answered by closing the menu the gesture had just opened. A launcher-opened
menu now survives that single follow-up track; typing, picking, Escape or a
pointer press still takes over.
The access-confirmation golden follows the panel's new highlight: it parks on
the row the options mark as the current value rather than the top row.
- The model seat decides before consuming: a focused control that is not a row
(a retry button) keeps the browser's traversal, and Tab enters the list only
from the trigger.
- A launcher-opened menu is a fresh intent, so the dismissal that closed the
same token earlier no longer silences it.
- Comments and JSDoc now match the implementation: the near-end entry resumes
from the walk's last index, Shift+Tab leaves whenever the menu is open, and
the anchor hand-back names the control that opened the menu.
- The e2e focus assertions all go through `expect.poll`.
- Regression coverage for a rowless pane (the trigger keeps the keyboard) and
for a focused retry keeping its Tab, in the seat and the popup shell.
- The PR records the two public-surface changes (`SessionInput.focus()` in,
`bindComposerFocus` out) and the row-fill focus decision.
Review follow-ups on the hand-offs this branch added:
- Menu gives the keyboard back to the control that opened it (an anchor may
wrap several, as a split button does), falls back to a disabled-free anchor
button, leaves a Tab it cannot use to the browser, and no longer moves focus
while the owner keeps the list open.
- The model seat settles only real rows with Tab and keeps the keyboard on its
trigger when a pane's rows are disabled or absent.
- The popup shell leaves Tab to the browser when nothing can be settled (the
error strip's retry button stays reachable), and a dismissal caused by a
stale catalog returns the keyboard like the other paths.
- A frozen-tier track is the submit gesture's own bookkeeping and no longer
re-arms a menu the user dismissed.
- The launcher reuses the shared caret-preserving focus helper; the option
contract records that `active` is the row an accept gesture confirms; the
conversation README documents the composer's key and focus behavior.
The dialog/menu Escape layering this branch also carried is out of scope here
and has been removed again.
The two split the other way around: the composer's triggers keep the focus
ring their design gives them (a 2px `--dsw-alias-border-l3` ring), while a
walked menu row shows the hover fill alone, without the browser's default
outline doubling it.
The menu family was the only place that suppressed the browser's default focus
ring. It keeps that ring now, like the app's other controls, on top of the
hover fill a walked row already carried.
Arrow navigation was bound to `autoFocus`, so the dropdowns that open with the
keyboard on their trigger — the composer's permission seat, the settings
pickers, the workspace and preset chips — had no walk at all, and a walk that
read its position from `document.activeElement` alternated between the first
and last row as soon as a row refused focus. The walk now works either way and
resumes from the index it last focused.
The keyboard returns to the anchor on Escape and on a selection — unless the
owner moved it itself, as a presented file card does for its preview button —
and the focused row wears the hover fill instead of the browser's ring. An open
menu takes the first Escape from the dialog it sits in.
The composer's trigger menu keeps its own Tab, which settles or drills the
highlighted candidate; Escape and Shift+Tab leave it without settling, and a
dismissed menu stays dismissed until its query changes. Opening it from the
launcher button returns the keyboard to the editor first, since the menu is a
combobox over that editor.
The popup select panel and the model seat hold focus while open, and their Tab
fell through to the browser's traversal, leaving an open surface that only a
pointer press clears. Both now settle on Enter or Tab, leave on Escape or
Shift+Tab, walk with the arrows, and park on the value in use: the panel's
highlight on the row its options mark current, the seat's drilled pane on its
checked row, with the keyboard handed back to the cell that drilled in when the
pane is left.
Dismissal rides the conversation's own focus(), so the caret returns where the
draft left it instead of at the start.
The install dialog asks the Host to read a spec before installing it:
`plugins/inspect` sorts the spec (registry name, absolute path, git address,
tarball), asks the registry through `pnpm view` in the profile directory, or
reads a directory's package.json, and refuses what would fail with
`plugins/inspect-rejected` and a problem the dialog words under the field.
A failed pnpm run carries a `kind` classified from how it ended and what pnpm
printed, shown as one line with the output folded behind the details.
Enabling happens after the fact from the installed screen, which scrolls the
list to the first new bundle. Refusals of the moment and the confirmed
cancellation are toasts; the run is stopped through the manager's
`cancelInstall` and the dialog offers the spec again once the Host confirms.
The non-pack dependency group folds by default under its own name.
The host config gains `inspectTimeoutMs`; the CLI passes it explicitly.
hiddenInset titlebar with traffic lights over a vibrancy sidebar, darwin-only
via data-platform set by the desktop preload: transparent chain to the window
material, native-theme sync from the app theme source, sidebar top strip +
full hide when collapsed with reopen controls in the new
conversation.session.header.leading slot, title-row drag regions, and
fullscreen right-sidebar traffic-light clearance via the dockkit strip inset.
A summary's fenced code block pins its banner at the scrollport top, where the
pinned compaction header already sits, so the header covers the banner's Copy
control and a click there collapses the summary. Stop the banner below the
header's band and square the pinned header's corners so its opaque fill masks
the scrolling prose at the edges.
With the plugin defaulting to on, the messages e2e case that asserts an
absent `dsh_session_log` must disable it explicitly; otherwise the stub
rejects the field it did not expect and the model call finishes as an
error. Also align the wire reference and feedback subsystem docs with the
default-on field.
The per-file coverage gate measures `packages/*/*/src` at 100% branches;
a short-circuiting `||` over the two lane markers leaves the second
operand unexecuted under Vitest. Testing the marker list instead keeps
the same default without an uncovered branch.
`@deepseek-ai/dsh-session-log-deepseek` now contributes `dsh_session_log`
unless a composition sets `enabled: false`. A recorded-Session lane
(`$VITEST` or `$DSH_SNAPSHOT`) keeps the default off, because its
committed fixtures are both replay input and expected output for the
persisted log; no fixture regeneration is needed.
Both blocks render their expanded body uncapped, so a long reasoning chain or
compaction summary carried the disclosure header off the viewport and buried
the toggle. Pin each header to the conversation scroll container while the
block is open, keeping the rule scoped to these two blocks so the capped tool
rows do not stack headers at the top of the viewport.
The compaction header also overrides its stacking rank above the summary's
code-block banner and keeps an opaque hover fill.
The workspace/changes event now carries only the turn number. The
recorder keeps each announced summary in memory under the event's
sequence and serves it through the new workspaceChanges service; the
Web deliverables plugin reads it once per announcement through an
authenticated summary route and renders the card only while the Host
still serves it, so a conversation reopened after a Host restart has no
card for its earlier turns. Snapshot objects move from a shared,
size-bounded store under the Harness home to a temporary directory per
Session that disposal removes, which retires the dshHome and
objectStoreMaxBytes fields. The cold-seeded fold scenario goes with the
logged file list; the client spec covers the fold and the recorded
scenario covers the served summary.
- files tree records its scroll offset in a ref and commits it once on
unmount, only while the owner's signal is live, so a scroll neither
re-renders the tree nor writes to a forgotten bucket
- LoadingIndicator drops iconOnly and always renders icon-only
- registry and the unviewable list share one suffix-matching module, and
register() rejects binaryExtensions entries outside extensions
- drop the ambiguous key suffix from the unviewable office group
- extract the text preview's header path block shared by the unsupported
and content branches; clsx for static class joins; CSS trailing newline
- sync the preview-operations and polish Agent Notes with shipped facts
The sidebar page no longer shows the profile template's own bundles
(dsh-base, dsh-web-app). The Settings Plugins section gets its read-only
Plugin list tab back for them, with both of its groups collapsed by
default. Restores @deepseek-ai/dsh-client-ui-settings-plugin-inventory
with its web bundle row, tsconfig entries, catalogs, and goldens, and
drops the built-in tag, locked switch, and source fact the page no
longer needs.
The deliverables group README links a subsystems page that carries the
PresentedFile, WorkspaceChangedFile, and WorkspaceChangesData
declarations and names the two log-only events; the persistence catalog
links the payload types to it.
The trajectory ledger now summarizes a recorded run_code call with the
program description instead of its raw JSON arguments, and marks it with
the code-inspection icon. Refresh the Auto-review denial golden so the
replayed ARIA expectation matches the shipped rendering.
Path helpers reuse isInside instead of re-deriving containment from
relative(); the recorder classifies uncovered file-tool hunks in one
pass over the canonical map; the card renders line counts through one
component and derives header and row gesture copy from the same phase
reader.
Both Host packages hand the turn's deliverables to the user and neither
is a filesystem capability, so they leave packages/fs for a new
packages/deliverables group. The Client package stays at
packages/client/ui-deliverables. Paths in tsconfig references and
aliases, gates, generated catalogs, READMEs, and the implemented notes
follow the move; archived notes and released schema records stay as
written.
Load target-owned Windows and macOS release settings, validate them before
preparation, and provide a configuration-only check command. Share the local
settings with upload, installer tests, and manual macOS signature checks.
Exercise English and Chinese installer fixtures using visible UI labels and
cover registered installation paths with trailing separators.
The packed worker deployment offers no realpathSync.native at plugin load, so
the home and temporary roots are now canonicalized asynchronously when a
Session first locates its repository. Windows-neutral tests replace chmod
and POSIX-only path expectations, and the right-Sidebar scenario enters
through the closing prose's file mention.
A completed native open no longer replaces the row's counts with a lasting
acknowledgement; only pending and failed opens do, for rows and the folder
header alike.
Review round fixes: a write that creates a file persists no hunks and
str_replace_editor persists none at all, so mutation calls now contribute
hunks from their arguments when the result carries none; each turn owns a
state object so a record still running for an interrupted turn cannot be
reset by the next turn; git add accepts exit code 1 from --ignore-errors;
rev-parse failures other than a missing repository fail loud; only a missing
index or store is tolerated; the temporary-root exclusion uses the repository
root; a row without a verified Host path previews in the Sidebar. The
recorded scenario now creates an ignored file.
Map @deepseek-ai/dsh-workspace-changes/types to src so the persistence catalog
links source instead of built output, and document every exported function
and recorder method.
Write every snapshot blob and tree into a per-repository object directory
under DSH_HOME/workspace-changes with the repository's objects attached as a
read-only alternate, so the user's repository gains no objects and the Harness
owns retention through objectStoreMaxBytes. The placement follows the
workspace change journal POC and costs nothing measurable.
Both archive verbs answer with the complete set, so a reply that lands
after a newer request or after a pushed increment overwrote it. The
Client model now installs an answer only while it is still the latest
archive-set request, and the follow increment and baseline supersede it.
Also narrow the page's public surface and its states: the locale
namespace stays internal, row derivation is a memo over the framework
hooks, the search input carries one accessible name, the empty-archive
message no longer covers a set whose entries cannot be restored, and the
new package declares no client dependency edge it does not import.
The Client Cordis catalog now publishes unarchiveSession on both the
Workspace service and the UI navigation capability, and the new Settings
section adds one nav row that every settings-dialog aria snapshot carries.
Refresh those expectations; the goldens gain only that row.
Sessions could be archived from the sidebar row menu but never restored:
the registry-global archive set was a one-way display filter with no
inverse RPC and no surface listing its members.
WorkspaceRegistry.unarchiveSession drops one id from archivedSessionIds in
one durable write on the same operation chain as archive, with no
session-existence probe because a removal cannot introduce an unknown
referent. The Workspace controller exposes it as workspace.unarchiveSession
returning the complete archive set, which the existing archived follow
increment already propagates, so no frame type and no persisted field
changes.
The new client package dsh-client-ui-settings-unarchive-sessions owns the
restore surface: one localized settings.section page listing the archive
set joined with loaded session summaries, newest archive first, searchable
by title or workspace, with one Unarchive action per row.
Vitest types `Reporter.onCoverage` as `unknown`, so a payload that carries no
istanbul `data` record now fails the partition instead of silently leaving
every location uncanonicalized; a spec case pins that rejection. The module
docs keep the contract and link the Agent Note for the mechanism, the
partition spec names what it asserts (the reporter argument, not Vitest hook
ordering), and the notes correct the `Infinity` origin to ast-v8-to-istanbul,
record the reporter-ordering dependency, and name the reporter each partition
child carries.
A source file carries one istanbul statement map per Vite environment, so a
node suite and a jsdom suite that load the same module enter the merged
report through two maps that spell one statement differently. istanbul
reconciles those overlapping ranges by attributing the hits of the narrowest
containing range, but only while a location carries numeric line and column
values. ast-v8-to-istanbul ends a whole-line statement at column Infinity, and
the partition blob serializes Infinity as null, so the merge command kept the
client-only spelling as an extra, unhit statement: a file whose every statement
ran reported 96.96% and failed the per-file 100% gate.
The coordinator now passes each partition a reporter whose onCoverage hook
rewrites every non-finite end column to a finite line-end column before the
blob is written, which restores that reconciliation without weakening the
gate. The uncovered-locations reporter reads the same column as a line end.
* docs(session): define the runtime and storage refactor plan
Document the staged SessionRuntime, SessionService, and SessionStorage design, developer transition rules, capability protocols, data compatibility, and FAQ in English and Chinese.
* docs(session): describe the switchable runtime-access gate in the plan
The plan said the access gate rejects concrete-class bindings. The
shipped gate is switchable: its checked-in default is `warn`, which
prints every finding, annotates it in GitHub Actions, and exits 0, so
branches that still bind `Session` merge while their consumers migrate.
`DSH_SESSION_RUNTIME_ACCESS=enforce` fails the gate for one run, and the
checked-in default flips to `enforce` once no in-flight branch binds
the class. Record that in the stage table and the enforcement section.
* docs(session): rename SessionRuntime to LogicalSession in the rebuild plan
The logical session interface is named `LogicalSession`; the plan, the
capability protocols, the developer transition rules, and the FAQ use
that name for the type, the detached factories (`createLogicalSession`,
`restoreLogicalSession`), the access gate (`verify-logical-session-access`,
`DSH_LOGICAL_SESSION_ACCESS`), and the prose that called the object a
runtime. The overview note moves to
`2026-09-06-logical-session-storage-rebuild.*` with a new pairing record.
The FAQ records why `SessionRuntime` was rejected: the package naming
table reserves `Runtime` for a component that runs live work, and the
interface holds one session's identity and log without running anything.
The late-listener case now carries a pending generation change, so its
assertions depend on the disposed guard, and the silent-generation cases assert
their tick counts.
Drop the shadow repository tier: a working directory outside any git
repository records nothing and the card stays absent, because a configured
exclude list cannot replace a missing .gitignore reliably. Seed a committed
repository in a new keyless Web scenario so the recorded turn exercises the
recorder, and retire the produced-files row from the affected goldens.
A notification repeating the published generation no longer withdraws displayed
options, so the invalidation observable and the README describe the same rule.
The composer seat hides on a cleared catalog while the slash picker keeps its
failure and retry, and the disposed guard that both callers already provide is
gone from the tick.
The package README, its Chinese pair, and the catalog spec now state both
public observables and pin the generation-change tick that withdraws displayed
options.
Add the dsh-workspace-changes Host plugin that snapshots the working tree at
turn start and end through a private git index, diffs the two trees, folds in
file-tool hunks for ignored and out-of-tree files, and appends a log-only
workspace/changes event. The Web deliverables plugin renders that event as the
changed-files card in place of the mutation-call row, with default-app and
common-folder opening through a new authenticated route.
Publishing the result of a read the picker waits for no longer closes that
picker, so a failed read keeps its error and retry state; a catalog
notification or generation change still withdraws stale options. Correct the
action-history rationale in the Auto review note and cover both paths with the
real popup controller.
Availability now follows the Session projection alone, so a failed catalog
read surfaces through the picker's own retry instead of hiding the command.
Type-guard the persisted denial reason before trimming it, and record in the
Auto review note why the reviewer reads the complete action history.
Loader settlement no longer rejects on a failed optional entry, so the Auto
rollback scenario reads the conflict from the startup audit. Refresh the Auto
denial golden for the merged composer chrome.
Register the permission catalog default response so the web roster boot
matches every Remote call it makes, and carry the deprecation rule for the
session history read this reviewer needs with the deferred migration
recorded in the package README.
Resolve 36 conflicts across source, bilingual READMEs, and generated
documentation: keep master's command-identity and popup APIs, RemoteMock
test infrastructure, and regenerated catalogs; keep the Auto review
catalog, slot, dismissal, and snapshot content.
ConnectionController deduplicates repeated connecting states across backoff
attempts, so the shell cannot tell manual retries from automatic ones; a
shell-held flag either flipped the label mid-hold or stuck across later
attempts. Every attempt now reads the single connecting label, and the
two-second recovery confirmation starts when the recovered pill becomes
visible rather than at the state transition, so the 800ms hold never
shortens it.
Master's `deprecate-synchronous-session-event-reads` marks `Session.eventAt`
deprecated and rejects new production calls. `liveEvents` predates that decision
and still needs the ordered log to reconstruct the preset a session runs under,
so carry the repository's standard line-scoped waiver for deferred migration.
Update the behavior tests for the static retry label, the 800ms minimum
connecting hold, manual-versus-automatic attempt naming, and the 150ms
fade-out unmount; restate the interaction in both packages' bilingual
READMEs and record the decision as an Agent Note.
Address the Code Entropy follow-up on d9c4f53758: the runner entry still
expanded its process-substitution object with a `readStdin` hook used only by
tests. Move that seam to `src/runner-internals.ts`, mirroring
`src/startup-internals.ts`, so neither entry grows package API for tests.
The managed-scope disposal cases in subprocess-local and bash-sandbox run past
the 5000ms default when this lane's four partitions, six workers per gate, and
sibling gates share one host. The same commit stays inside the budget on the
in-house pool: the hosted image exceeded it (run 34449848541) and the lane is
green with 90000 (run 34457655892). The Windows coverage lane already grants
the same budget for the same class of teardown.
(cherry picked from commit bf3468adf0947a51f09d4a1aaae2865f3e50a2f2)
The ACP plugin-config case drove a real dispose with 150ms graces. On the hosted
image the SIGKILL escalation landed while the managed scope could not take the
signal — systemctl failed the kill ("Failed to send signal SIGKILL to auxiliary
processes: Invalid argument") — so the run reported a teardown failure the
configuration never asked for. The case asserts that config graces reach the
real run, so the values only need to clear the host's scope handling. Its mock
refuses stdin EOF and SIGTERM by design, so the case waits out both graces and
carries its own budget for that fixed cost.
Both illegal-UTF-8 residual cases relied on a plain sched_yield leaving each
write as its own read chunk. A starved reader coalesces hundreds of bytes into
one chunk, which inflates the measured peak for correct accounting too: the
hosted image measured 2563 against the 2048 bound. Pace with a real sleep so
each byte is its own chunk everywhere, and keep each payload above the bound
(3200 bytes; 1100 CESU-8 sequences) so a raw-byte undercount still flushes past
it instead of only at EOF.
(cherry picked from commit 30e0c3ff0415befe3652a55611df8a18d82c18bb)
`disposal contains a spawn-failure rejection that races teardown` asserted one
winner of the race. A bootstrap that publishes its pre-exec failure rejects with
that failure, and a teardown that stops the bootstrap first settles as the
requested termination — the recorded failure only outranks the stop when it was
published before the stop landed. Assert the contract instead: a rejection
carries the failure, a stop carries SIGTERM, which only the Linux scope records
because the win32 job owner and the fallback launcher both reject the start.
The two terminal cases that drive a mocked PTY exit state why they pin
`internals.platform`: under the host's native scope the mocked exit races the
scope bootstrap and fails as `terminal scope exited before its bootstrap
consumed the launch request`.
(cherry picked from commit fee113eb9a5632555304858d2bf6355588a3aaf0)
The disconnected pill now names the retry action statically (refresh icon +
"连接异常,刷新重试") instead of swapping text on hover; connecting shows a
spinner, holds visible for at least 800ms, and distinguishes manual retry
(重新连接中) from automatic backoff (自动重连中); state changes fade over
150ms. Pill chrome tightened to 28px height with a 20%-alpha label border.
The failover leg runs this suite on pools this repository does not own, and the
coverage lanes failed there on host properties their cases never named: the
containment a mocked PTY exit raced, the grace a managed scope needs before it
can take a SIGKILL, reader coalescing in the illegal-UTF-8 residual cases, and
a Windows image that refuses the folder dialog class. Record what each case
pins now, what was rejected, and the runs that measured it, so the next pool
change starts from those host dependencies instead of re-diagnosing them.
The managed-scope disposal cases in subprocess-local and bash-sandbox run past
the 5000ms default when this lane's four partitions, six workers per gate, and
sibling gates share one host. The same commit stays inside the budget on the
in-house pool: the hosted image exceeded it (run 34449848541) and the lane is
green with 90000 (run 34457655892). The Windows coverage lane already grants
the same budget for the same class of teardown.
The ACP plugin-config case drove a real dispose with 150ms graces. On the hosted
image the SIGKILL escalation landed while the managed scope could not take the
signal — systemctl failed the kill ("Failed to send signal SIGKILL to auxiliary
processes: Invalid argument") — so the run reported a teardown failure the
configuration never asked for. The case asserts that config graces reach the
real run, so the values only need to clear the host's scope handling. Its mock
refuses stdin EOF and SIGTERM by design, so the case waits out both graces and
carries its own budget for that fixed cost.
Both illegal-UTF-8 residual cases relied on a plain sched_yield leaving each
write as its own read chunk. A starved reader coalesces hundreds of bytes into
one chunk, which inflates the measured peak for correct accounting too: the
hosted image measured 2563 against the 2048 bound. Pace with a real sleep so
each byte is its own chunk everywhere, and keep each payload above the bound
(3200 bytes; 1100 CESU-8 sequences) so a raw-byte undercount still flushes past
it instead of only at EOF.
`disposal contains a spawn-failure rejection that races teardown` asserted one
winner of the race. A bootstrap that publishes its pre-exec failure rejects with
that failure, and a teardown that stops the bootstrap first settles as the
requested termination — the recorded failure only outranks the stop when it was
published before the stop landed. Assert the contract instead: a rejection
carries the failure, a stop carries SIGTERM, which only the Linux scope records
because the win32 job owner and the fallback launcher both reject the start.
The two terminal cases that drive a mocked PTY exit state why they pin
`internals.platform`: under the host's native scope the mocked exit races the
scope bootstrap and fails as `terminal scope exited before its bootstrap
consumed the launch request`.
A Windows Server image can answer CoCreateInstance(CLSID_FileOpenDialog)
with CLASS_E_CLASSNOTAVAILABLE, so the win32 COM smoke has no dialog to
open and the POSIX rejection expectation is the only proof that image can
produce. Gate the pair on a PowerShell activation probe taken outside the
code under test, so a GUID or vtable regression in src still fails the
smoke instead of skipping it.
Address the Code Entropy finding on f67d722243: the `./startup` entry exported
`internals` only so tests could substitute process facts, which added avoidable
package API. Move the seam to `src/startup-internals.ts`, which no package
export names, and have the provider and its tests import it from there.
Range observation waited for an active transient scope to leave `active`
before it would prove the managed range empty. systemd ends a scope only when
it observes the populated-to-empty transition, so a payload killed before the
manager placed it in the scope's cgroup leaves the unit active with an empty
cgroup forever. `waitForExit()` then polled a unit that could not change, and
`LocalSubprocessRuntime.disposeManagedProcesses()` awaited it without bound —
teardown hung for any command cancelled immediately after spawn.
Each state query now also reads the unit's process count. Once termination was
requested and the launcher has exited, a still-active unit reporting no
processes is the empty range: the launcher forked every process it will ever
fork, so nothing later joins that cgroup. The owner stops the leftover unit so
transient units cannot accumulate, and an unreported or `[not set]` count
stays unknown and keeps waiting.
The race needs a live user-systemd manager, so it stayed dormant wherever the
native scope path was unavailable and surfaced once CI routed lanes onto the
Blacksmith hosted image, whose runners accept that path. Ten repetitions of
`local.spec.ts > disposal kills still-running processes and awaits their exit`
in a Linux container with a user manager failed seven times before the change
and passed ten times after it.
Address the ds-review-bot suggestions on 4099b84ee8:
- The post-resume comment scopes the write lease to other processes, so it no
longer contradicts the post-idle re-check that defends against a same-process
overlay writer.
- The Agent Note attributes `pnpm-lock.yaml` to the new
`@deepseek-ai/dsh-session-query` workspace link instead of the shared chunk,
which `scripts/check-workspace-constraints.ts` and the manifest publish.
- The package summary keeps "leaves nothing running behind", so it does not
imply the runner leaves no persisted Session behind.
The package README and the Agent Note named an extension or error code but
omitted the unrelated-message case the classifier also rejects, and the note
loosened "Node's unknown-`.css`-extension refusal" to any non-`.css` failure.
The built-bundle import sweep pinned the Dockkit exemption to
`packages/client/ui-dockkit/lib/components/dockkit.module.css`, but the built
bundle imports `@deepseek-ai/dsh-client-ui-primitives` before its own
stylesheet, and the tsx launcher resolves that specifier through tsconfig
`paths` into the dependency's `src` tree. The sweep therefore reports
`packages/client/ui-primitives/src/StateDot.module.css`, so the pinned path can
never match where client build output exists; the Windows complete-gate
inventory reports the exempt bundle as an unexpected baseline failure.
Admit Node's unknown-`.css`-extension refusal for any stylesheet and keep every
other failure a finding. The classification controls cover an admitted Dockkit
stylesheet, the dependency's source stylesheet, another extension, an arbitrary
message, another error code, and a stale exemption.
Agent Note: .agents/notes/implemented/bug-fix/2026-09-10-built-bundle-css-exemption.md
Address the ds-review-bot findings on 8724223621:
- The JSON error override throws commander's control-flow error instead of
delegating to `program.error`, whose own printer wrote an unprefixed
`error: …` line to stderr and broke the documented `--json` contract.
- Cover the post-idle adoptability re-check with a focused case: an overlay that
appends a preset while the runner awaits idle is still rejected, and the stale
"live Session" comment now names that residual writer.
- Scope the Agent Note change sentence to the product change so it no longer
claims the whole patch is confined to one package.
Address the review suggestion that a caller-provided `cwd` left
`tempDirPrefix`/`tempDirParent` as dead but still-required inputs. The options
are now a union of "expand a prefix into a fresh temporary cwd" and "reuse this
existing directory", so the type system rejects passing both; the two callers
that reuse a directory drop their unused prefix.
Address the ds-review-bot findings on 472554a29e:
- resolveAgent refuses an Agent already live under the requested id: its owner
may still drive it, and `whenIdle` is not a single-message signal, so the
one-shot runner cannot own an exclusive run interval over it. The adoptability
rules still run first so the refusal stays specific.
- Refresh the README intro, identity section, and limitations, and the Agent
Note live-identity rule and change scope, for adopt-only.
- Clarify the loader-smoke `cwd` JSDoc (the prefix/parent inputs are ignored)
and pin the resumed wake's final text in the two-wake e2e.
Turns running at or past 3600s now read 1小时05分03秒 / 1h 05m 03s in the
running clock, the settled Ran-for footer, and the turn-time dialog, which
share formatRunDuration. Below an hour the second and minute branches are
unchanged, and the hour rolls at the unit boundary so 60分00秒 never appears.
Address the ds-review-bot findings on 9aea4b0804:
- Drop the stale "freshly created" wording from the resolveAgent `@returns`
and rewrite its leading comment for the two remaining identity paths.
- Rewrite the README run-flow paragraph (EN and ZH), the intro sentence, and
the cordis.patch.yml header comment for adopt-only.
- Add a real-composition two-wake e2e: capture the generated id from the first
`--json` wake, resume it by `--session-id`, and assert one persisted log holds
both tasks. `runLoaderSmoke` gains an optional caller-owned `cwd` so both
wakes share one world; the harness leaves that directory in place.
`--session-id` now adopts only: an id with no stored Session fails before the
task runs instead of creating an empty history the caller believes it is
resuming. A first round omits the flag, and the runtime reports the minted
`session-<uuid>` identity in the opening `session` event, so a supervisor
persists it and names it on every later wake.
Updates the help text, the README pair, the Agent Note pair, the generated
config catalog, the unit suite, and the product-profile e2e.
Resolve the markdown-preview revert and README proofreading against this
PR's trajectory inspection work:
- CodeBlock keeps `showHeader` (the PTC code inspector supplies its own
toolbar) on top of master's `contentRef` scrollport wrapper.
- ui-primitives README records both `contentRef` and `showHeader`.
- ui-trajectory README keeps master's proofread wording alongside this
PR's PTC inspection and timing paragraphs.
- Re-record the README.i18n.yaml pairing hashes for the three merged pairs.
Address the ds-review-bot v12 critical on PR #3849: `addUsage` published the
sum of the attempts that reported usage while silently dropping the ones that
did not, so `step_end.usage` looked exact but under-counted billed tokens.
Track completeness per step and omit usage entirely when any attempt in the
step reports no sample, matching `deriveTurnTokenUsage`'s fail-closed rule.
Address the ds-review-bot v11 review on PR #3849:
- Accumulate every attempt's usage into `step_end.usage`, including a retried
attempt whose only sample sits in its discarded `assistant/attempt` stream.
- Re-check the adopted Session after the idle wait, so a preset selected in
that window is still rejected.
- Reject a whitespace-only `sessionId` supplied through a Cordis overlay, which
bypasses the CLI trim check.
- Narrow the documented error contract: a profile whose own plugins fail to
load exits before the runner mounts and keeps only stderr diagnostics.
Address the ds-review-bot v10 review on PR #3849: `boundJsonLine` bounded the
serialized JSON but not the newline the writer appends, so an event that
serialized to exactly 32 KiB produced a 32 KiB + 1 byte record. Reserve the
terminator byte inside the cap, document the cap as newline-inclusive, and
assert the written record length in the boundary tests.
Address the ds-review-bot v9 review on PR #3849:
- Require `sessionQuery` before the live fast path too: a later process has no
live Agent and must find the id through the query service, so a live identity
no longer bypasses the check.
- Soften the live `stat` comment to what it proves: a stored record rules out an
in-memory registration, while write-handle ownership is not queryable.
- Complete the `parseArguments` JSDoc and fix the broken English sentence in
README.md and the Agent Note.
Address the ds-review-bot v6 review on PR #3849:
- A persisted `agent-preset/selected` record whose `data.agentPreset` is not a
non-empty string used to read as "no preset", letting the run continue under
this bundle's composition. It now fails closed.
- A live identity now has to be known to `sessionPersistence` (`stat`), since
an Agent registered only in memory has no write handle and would flush
nothing.
- Tool arguments that JSON cannot round-trip (an overflowing number such as
`1e400`) keep their raw text instead of the `null` JSON.stringify reports.
The failover runbook, the node-compatibility note, the native-Windows note,
and the serial-reference note record the third variable value's scope and
exclusions (bilingual, pairing re-recorded); a new implemented process note
owns the value's participating set, the dormant-by-default canary posture,
and the walker-fix rationale. .github/AGENTS.md points at both notes.
Node's internal fs.glob, from some 24.x releases, lstat-probes
<matched>/<next segment> while expanding a ** pattern over a symlinked
file and throws ENOTDIR instead of skipping (observed on node 24.13.0
scanning snapshots/acp/image-compaction's symlinked
system-prompt.expected.md, which failed verify-md-wrap inside
check:ci:static). uniqueRepoFiles now walks with dirent types, never
probing a path under a file, and mirrors fs.glob's dot:false and
follow:false semantics: the four repo-files gate pattern sets enumerate
identically. Broken or cyclic symlinks fail loudly in the caller's
realpathSync, and pattern forms the walker does not model (metacharacters,
a trailing **, empty segments) are rejected up front. repo-files.spec.ts
pins the symlink, broken-link, and rejection behavior.
Address the ds-review-bot v6 review on PR #3849: the guard sat before the
create branch only, so adopting an already-live identity could still run a
session whose history dies at exit. Move it to the top of resolveAgent, where
it covers both the live and the create path, and add a rejection test for the
live path alongside the existing create-path one.
The per-platform failover repository variables (DSH_CI_FAILOVER_LINUX and
DSH_CI_FAILOVER_WINDOWS) accept a third value alongside the existing
selfhosted routing. Set by a repository writer, the value retargets the
always-on pull-request and master lanes onto the matching hosted-runner
labels (16-core pools map to the 16-vCPU tier, standard legs to 4-vCPU);
unset or any other value keeps today's runners, so the leg is dormant until
a writer opts in - a canary path for capacity fallback, never the default.
Participating jobs: the three enterprise Linux workers, the node-compat
legs, the four Windows lanes, and the all-checks-passed verdict in ci.yml;
expected-filenames; the sandbox bwrap leg; and the two manual benchmark
matrices (4-32 vCPU tiers map to equivalents; 64/96-core rows keep the
fleet labels). Everything pinned to its own runner by spec, credential, or
host capability keeps it under every value, and the release-shaped chains
stay entirely hosted. request-review.yml was retired on master while this
work was in flight and the branch takes the deletion.
Specs pin the new branches: ci-workflow.spec.ts asserts the labels appear
in the extended selectors, and ci-compatible-selfhosted.spec.ts evaluates
the routing mode.
Address the ds-review-bot v6 review on PR #3849:
- Require the composed sessionPersistence service before creating an exact
--session-id identity, so a profile that keeps sessionQuery but drops
persistence exits 1 instead of printing an id whose history dies at exit.
- Stop exporting the now-internal boundJsonEvent and describe it in the
present tense; its test now exercises boundJsonLine.
- Say "64 levels or deeper" in the README and Agent Note, matching the
depth budget (an input keeps 63 nested containers, the 64th is cut).
Tests: 77 headless unit tests with per-file 100% coverage on src, 14 profile
e2e tests, constraints/typecheck/lint/doc/pairing gates.
Address the ds-review-bot v6 review on PR #3849:
- Cut a projected payload nested deeper than 64 levels instead of recursing
until the stack overflows; the isolated session listener would otherwise
drop the `tool_call` silently.
- Write both `error` events (startup usage errors and runner failures) through
`boundJsonLine`, so the documented 32 KiB line cap covers JSON-escaping
expansion as well as the 8 KiB message cap.
Tests: 76 headless unit tests with per-file 100% coverage on src, 14 profile
e2e tests, constraints/typecheck/lint/doc/pairing gates.
Address the ds-review-bot v5/v6 review on PR #3849:
- Register the shared `lib/json-stream-*.js` chunk in `packageFileExtras` and
order `files` as the workspace-constraints gate requires, so
`pnpm run constraints` passes.
- Re-validate the preset on the Session `agents.resume` actually attached,
closing the TOCTOU window between `observeSession` and the write lease.
- Bound one serialized event line at 32 KiB: an over-long event keeps its
scalar fields, drops structured ones, and at the extreme reduces to `type`.
- Normalize an empty tool-argument string to `{}`, matching the executor.
- Scan only real `--json` flags (stop at `--`, skip a `--session-id` value)
before installing the JSON error override.
Tests: 74 headless unit tests with per-file 100% coverage on src, 14 profile
e2e tests, constraints/typecheck/lint/doc/pairing gates, tarball import smoke.
Move exact user-patch watching into app boot and audit activation explicitly.
Wait for CLI fallback HMR and preset subtrees. Join the first fiber-disposal
result in the directory chooser and browser package runner. Keep async
update errors observable without restoring generic Loader rollback.
Restore the current workspace-link and Schemastery manifests and their gate.
Preserve unconditional patch-input cloning, awaited Include initialization,
and durable teardown writes. Update diagnostics, snapshots, bilingual docs,
and the exhaustive vendor divergence ledger.
Validation: focused Loader, watcher, preset, inventory, and cleanup tests;
100% watcher coverage; 32 Web preset cases; real CLI webhook model session;
SDK smokes; 13 headless expected cases; 3 recorded-session replays; build,
typecheck, lint contracts, and all 34 doc-sync gates.
A full Node 24/Python 3.12 Vitest run had 21806 passes and two failures:
a baseline-reproduced experimental CSS import failure and a Python probe
timeout that passed alone. The default Node 26/Python 3.9 run additionally
exposed unsupported Python and FileHandle-GC host failures.
Apply real git revert --no-commit operations in reverse order:
acb0797a1421eab128a4bd03355e930c1a221fb0
af97714f2413a3e55f0f15ab1b283beb133b3f2d
2018b0b7e64b444bff82b62c9466f13adc4603bb
43170c027cfbf9155dad7a43c01ca39364d4aef2
a621e927d6b3763798c3929e28912d86c9576f2d
Resolve the app-boot package move, preserve deleted files and frozen archived
notes, retain the modern lockfile graph, and preserve independent later
lifecycle and lazy-config changes. Do not revert 6ac7d72d: that merge also
contains eight unrelated commits from the #936 dependency chain.
This is the mechanical intermediate result. Its focused check exposed
consumer and packaging incompatibilities. The following compatibility
commit supplies those adaptations and reconciles the vendor ledger.
Address the ds-review-bot v6 review on PR #3849:
- Publish the `lib/json-stream-*.js` chunk that both entry points import.
Without it the installed tarball failed to load with ERR_MODULE_NOT_FOUND,
because `files` listed only `lib/index.js` and `lib/startup.js`.
- Resolve a Session's preset from its log (`agent-preset/selected`) instead of
only the immutable creation header, so a Session that switched preset while
blank is rejected too. Both the persisted and the live path scan the events.
- Write the `--json` error event for Commander grammar rejections (an unknown
option, a missing option value) as well as the action's own usage errors, and
drop Commander's `error: ` prefix from the event message so one event type
carries one message shape.
- Reject a whitespace-only positional instead of falling back to stdin, so an
accidental blank argument cannot consume a pipe.
- Document the stdin task as verbatim and the widened usage-error contract.
Tests: 68 headless unit tests with per-file 100% coverage on src, 14 profile
e2e tests, plus a packed-tarball import smoke check.
- Reject adopting a Session recorded under an agent preset, and one that
recorded no cwd; align the subagent/forked diagnostic wording.
- Write the bounded JSON `error` event for a --json usage error before exit,
since the runner never mounts to write it.
- Keep the caller's exact --session-id instead of the trimmed value.
- Bound object keys and copy a literal `__proto__` key as data rather than
through the inherited setter.
- Reject a lone `-` mixed with other task words.
- Document the in-turn failure contract and step-commit text/thinking timing.
Registers slash/input-pick-files with the cordis catalog walk exemptions
beside its sibling slash-input events, and points the web e2e at the
'Add files or run commands' launcher and the capitalized row titles the
paperclip removal and the menu presentation introduced.
A localized claim token (/目标 ) made the composer look up hint.目标, miss the
dictionary, and fall back to the Host's usage string. CommandClaim now carries
the catalog name beside the display token, the state projection keeps it, and
the hint lookup reads it.
Takes the merged slash-command localization (#3557): its zh description
copy replaces this branch's, its contribution description() callback and
HOST_DESCRIPTION_KEYS are superseded by the label/description/icon
contribution face and presentation.ts, and its zh menu golden and
locale-switch test are refreshed for the sectioned menu.
The card's title button stretches over the card, so a click anywhere opens
the page while the switch and the Add to… menu sit above the stretch and keep
their own clicks; descriptions are one ellipsised line again. On the page,
the description fills the column, and retry and uninstall sit in the head
beside the switch instead of at the bottom-right. The card surface binds the
elevated scrollbar pair the ui-theme rule asks of a sheet that scrolls.
The package hosts the sidebar's Plugins panel and a preset's capabilities
section, not a settings tab; the directory, the package name, and its
dictionary namespace (`pluginManager`) say so, and every reference, catalog,
and note follows.
The card's chevron button goes: the title is the text button into the page,
underlined on hover, with the same accessible name. Descriptions wrap to two
lines instead of one ellipsised line.
Master's left sidebar grew global main panels, so the manager leaves the
Settings dialog: `ui-settings-plugin-manager` registers a `sidebar.panellist`
entry and the `main` panel it opens under one id, `plugins`, and the former
tab becomes `PluginManagerPage` with its own heading, intro, refresh, and
Add plugin, scrolling inside the main column. Plugin configuration stays in
the Settings Plugins section, which `ui-settings-plugins` now renders
without a tab strip when it holds one contribution.
Web e2e reaches the manager through the sidebar; the plugin-config,
plugin-manager, settings-chrome, and lifecycle-chrome scenarios and goldens
follow. The decision and why configuration does not move are in the new
Agent Note, cross-linked from the two notes it revises.
The merged master text put docs/architecture.md over its 2400-word ceiling;
the details this paragraph repeated already live in packages/boot/app-boot.
Awaiting inside each arm of the scope ternary left v8 with a negative else
count on the response check, which the coverage gate read as an uncovered
branch; the request is now chosen first and awaited once. The breadcrumb
separator was a 1px filled divider where every other neutral line is 0.5px.
The duplication gate flagged three clones the plugin manager and the scoped
settings cards had grown: the bundle enable switch on a package's card and
page, the last-operation notice on the package list and a preset's section,
and the subscribe/bind/publish plumbing of CardForm and ScopedCardForms. Each
now has one owner: EnableSwitch, NoticeLine, and the ObservableForm base.
The tail dropped a whole kept chunk whenever a later chunk pushed the total
over the limit, so a flood that arrived as one read (Linux) left only the
bytes written after it. `StreamTail` trims the oldest kept chunk instead and
is unit-tested with both read patterns.
`disableBundle` now moves a package from `dsh.profile.bundles` to
`dsh.profile.disabledBundles`, and `reconcileInstalledBundles` enables only
a bundle named in neither list, so an unrelated pnpm run never re-enables a
bundle the user turned off while an update that gives a dependency
`dsh.bundle` still activates it (the built-bin e2e scenario the previous
"new dependencies only" rule broke). The record goes when its dependency
does.
Also: `visitIdentifiedRows` in patch-rows owns the per-patch restatement
tracking that compose-stack and external-bundles had cloned (duplication
gate), and the probe spec's flood fixture retries short and EAGAIN writes
because the child's stderr pipe is non-blocking once tsx touched
process.stderr (the test raced the parent's drain).
master now bounds a README Summary at 100 words; the boot group, the plugin
manager, and its host face say the same in fewer. The zh config catalog gains
the paragraph break the merge dropped.
The root entry now exports only what the CLI, the plugin inventory, the
plugin manager, and tool-cordis use, plus the parameter and result types of
those operations. The contained group and its registry, the external-layer
composition, the row-ownership helpers, the probe format number, and the
runtime option type stay module exports the package's own tests import.
The profile runtime reads user-disabled rows from the committed composition
now, so the e2e scaffold's stack carries the empty set and no longer hands the
runtime a patch reader.
The view attributes a disabled row through profileRuntime.userDisables, which
walks the groups holding the entry, and the manager spec stops passing the
runtime option the profile runtime no longer takes.
The inventory keeps dsh-app-boot an optional, type-only peer, so the walk
over an entry's owning groups moves behind ProfileRuntime.userDisables(): a
consumer with the runtime asks it, one without reports composition as before.
The module graph and the cordis catalog follow.
The probe compared two resolved URLs, which differ for one package under a
source launch (src against lib), across a symlink, and behind the proxy a
packaged executable writes; it now walks each URL up to the cordis manifest,
follows a dsh module proxy to its target, and compares real package
directories, with a probe test for the linked and the proxied case. The README
records that the probe child itself cannot run from a packaged executable.
The runtime's user-disabled row ids now come from the committed composition —
composeProfileStack collects them from the user layers it composed — so a
rejected or half-written patch file no longer makes the list disagree with the
running tree or fail to parse. The inventory attributes a row disabled through
a group the user disabled to the user, through the shared userDisablesEntry
walk, and lists a user-layer row the composition left out under the new user
trust instead of builtin.
The probe child now runs with the harness's credential-shaped variables
scrubbed — the one rule every spawner uses, now defined in
dsh-launch-environment and re-exported by dsh-subprocess — and its report is
the one message echoing a per-run token the child removes from its environment,
together with process.send, before importing anything. The failure text keeps
the last 16 KiB of stderr instead of every byte, and the probe settles only on
the child's close, so no pipe or channel outlives the call.
reconcileInstalledBundles now enables only the dependencies a run added, so
a bundle the user disabled keeps its state through an unrelated install or
removal. Row registration records the group each row is declared under: a
config override restates a row only under that group, while the same id twice
in one list or under another group is a duplicate — a contained bundle is left
out and a built-in layer fails loud, instead of the Loader rejecting the update
or moving the row at mount.
Move the IMAGE_OFFLOAD_REQUIRED recovery out of llm-retry into the new
@deepseek-ai/dsh-compaction-image-offload package, a sibling of the
tool-result pruner: image offload is compaction in another capacity
dimension. Each replaced node is priced through the seam's compaction/prune
shadow-price protocol before its marked copy lands. llm-retry returns to
master; the base bundle mounts the executor.
The subprocess runtime rejects a handle's done with the cancellation reason
when it terminated the managed range before the target started, which the
Windows runner does whenever an abort or kill() races the launch. Both
executors filed that rejection as a provider failure, so result() re-threw
the caller's own AbortError instead of resolving the aborted or killed
classification. A live handle whose rejection follows this execution's own
kill() or spawn-signal abort now settles as that terminal outcome; a
synchronous spawn throw, which never produced a handle, stays a failure.
The background branch of the synchronous ENOEXEC case still looked for the
pre-merge `spawn failed:` note; the merged providers render master's
stage-neutral `subprocess failed before reporting an outcome:` on that
path.
The scenario's barrier plugin patched ctx.jobs.wait, which the consolidated
seam no longer has: waits live on the operations forCaller hands out, so
the hold file was never removed and the mock ACP server never released the
prompt. The barrier now wraps forCaller and releases on that view's wait.
The Host removes a rejected package under that package's name, so its
pnpm run streamed under a spec the dialog did not match and never showed.
The Host runs one mutation at a time: while the dialog's install is in
flight every chunk is its own, so the spec filter goes.
The install dialog draws every pnpm run with the shared TerminalBlock: the
command line, pnpm's coloured output streaming in while it runs, the exit
code when it is not zero, and a line naming the profile directory it installs
into. A finished run leaves Done as the only action; editing the spec starts
over, and a failed run offers Retry.
TerminalBlock now shows the output printed so far while a command runs and
knows a settle without an exit code; cyan and bright cyan map onto two static
blues so pnpm's numbers stay legible on the light theme.
Every chunk now carries the directory pnpm ran in beside its argv, so a
renderer can say where the install lands. The generated event graph and the
cordis API catalog follow the chunk type.
The installer now decides pnpm's colouring per consumer: the Web host keeps
colours on for the install dialog's terminal, the CLI turns them on only when
stdout is a terminal, and a plain log strips what a colour=always pnpm still
prints. Every chunk carries the argv it came from, so a renderer can label
each pnpm run.
The permission selector and IconShieldOutline16 now read one exported
SHIELD_OUTLINE_PATH; the icon-set header no longer claims every glyph is
filled; the three menu glyph docs name what they draw and where they are
used; the compact ring matches the set's extent; the icon pin asserts the
three names.
The + button and a typed / open one menu with an Add section (File, Goal,
Plan, Feedback) and a Commands section (Compact, Permission, Model, Export)
in usage order; rows carry a glyph, a localized title and description, and
the command name as an alias where the localized title differs. Built-in
Host rows get their face from the ui-commands dictionaries when the catalog
description is the canonical English text. A menu pick fills the locale's
claim token (/计划 ) while the submission executes the catalog name, and a
typed localized token resolves on Space and Enter in every locale. The File
row is a new action contribution kind that opens the composer's file picker
through the scoped slash/input-pick-files event; the paperclip button is
removed and the + button reads "Add files or run commands".
Drop the image/offload event and every session change with it. An adapter's
IMAGE_OFFLOAD_REQUIRED failure now makes dsh-llm-retry replace the surface
nodes carrying the named oldest images with copies whose blocks are marked
offloaded, through the same surfaceOp: replace compaction uses, then retry the
step. ImageBlock.offloaded becomes a durable field the routes project and the
token meter prices; the session core, its event map, derivation, and
validation return to master.
A disable — of a pack or of one row — used to open the confirmation at
once, showing "checking" until the Host answered, and close it again when
nothing depended on the target: a flash of a dialog for every switch-off
that needed none. The Host is now asked first, with the switch inert
meanwhile, and the dialog opens only for a target something depends on; an
uninstall keeps opening at once, since it is always confirmed. A check
that fails on the wire no longer stands between the person and the
action they asked for.
A contained row left pending is recorded as waiting for a service, but a
live recomposition that brings the service back — a sibling row switched
on again — activates the fiber in place, with no create() to clear the
record, so the registry kept reporting a wait that was over. The group now
watches fiber states once it has recorded a wait and clears the record
when that row's fiber comes to life.
Switching a row off first asks the Host what other rows inject of what the
row provides — the package-level dependents answer filtered to the services
this row provides — and waits for the same confirmation a pack's disable
uses, naming those rows; a row nothing depends on switches off at once, and
switching on never asks. A row whose fiber is active is running whatever
record the failure registry still holds for it, so a stale wait no longer
reads as waiting once the service it waited for is back.
A row waiting for a service another row provides reads the same whether a
live recomposition left its fiber pending or a fresh composition recorded
the wait as an inject-pending failure: waiting for a dependency, on the
row, in the count line, and on the pack's tag in warning tone, with the
pack's reason line left to the row that explains it; the red problem is
kept for a real failure. A module's note about where it is composed now
wraps under its name, so a long list of presets no longer squeezes the
name into a column, and the module's own Add to… is all that sits at the
right.
The list splits into Plugin packs and Plugins, each with its count, in
place of one Installed group; a library counts among the plugins. Retry
and uninstall close a package's page at its bottom-right as outlined
buttons, uninstall in red. A row that is off says so in two words: Off
when the person switched it off, Off by default when the pack keeps it
off; the reason it cannot be switched on stays on the locked switch.
The earlier row block stayed in the stylesheet beside the new one, so a
row still laid out as a three-column grid and squeezed its whole line into
the seven-pixel first column, one character per line.
Each row on a package's page is an entry: a plugin icon, the row id over
the module it loads, its state with a dot at the right, then its switch;
rows sit on the page ground one hairline apart, so they read on a white
page too, where the filled bars vanished. A failure keeps its own line
under the entry. The modules a plugin declares addable read the same way.
A finished enable, disable, retry, uninstall, or added row no longer posts
a notice: the re-read list shows the new state, and only a restart the
change waits for, or a failure, has something to say.
A card keeps the summary — name, one-liner, tags, its switch or Add to… —
and opens the package's page behind a crumb, as a preset card opens its
detail page: the version and source; a pack's rows as a list, every row on
one line with its state, its failure when one is recorded, and its switch
where the pack is switchable, with a filter past ten rows; the built-in
rows the pack changes; the modules the package declares addable, each
saying where it is composed, with an Add to… of its own; and retry and
uninstall. The expanded card and its component chips are gone with it.
The two `ts` fences declare their inputs and import what they use, so the
doc-typecheck gate compiles them against the workspace API instead of
failing on the names the surrounding prose supplied.
Each component chip of an external pack composed on a live-reload profile
carries a Switch that calls plugins.setRowDisabled against the profile's
global user layer: off writes `disabled: true` for the row id, on deletes
the key, and the tree recomposes at once while the rest of the pack keeps
running. A component the pack itself keeps off is locked with the reason,
since the user layer only denies; a failing component can be switched off;
a row another layer owns has nothing to switch. Built-in packs, packs that
are off, and profiles that apply patches at their next start keep the
read-only chips. Only the row with a write in flight goes inert.
Drop Session.imageOffloadWatermark(), which no production code read once
recovery moved to llm-retry; shorten the append and seed validators; keep
offloadedImagePrefixCount and the represented-bytes arithmetic private to
requiredImageOffload, their only caller.
`src/index.ts` held the installer, the manager, the view fold, and every
helper in one 1100-line file. It is now the package API alone, re-exporting
from `installer.ts` (pnpm runs, the probe cache, post-install checks),
`manager.ts` (every operation over the booted profile and the mutex),
`view.ts` (the package view fold and the row-ownership walk the dependents
query shares), `modules.ts` (declared-module row naming and views), and
`helpers.ts` (the diagnostic prefix, tooling bounds, spawn seam, and manifest
readers). The public surface, the tests, and the failure codes are unchanged.
The `cordis.patch.yml` parser and the comment-preserving writer are the
Loader's format, not a mechanical primitive: they build on the include
plugin's dialect and the Loader's row types, which no other `util` package
does. They now live in `app-boot` as `src/patch-file.ts`, exported as
`@deepseek-ai/dsh-app-boot/patch-file` with its own bundle entry, so the
agent-preset roster and the plugin manager load them without the boot entry.
`dsh-patch-file` goes, with its README and catalog entries; `yaml` moves into
app-boot's dependencies.
A dozen plugin packages already depend on `dsh-app-boot`, so the roster
joining them costs nothing; the note records why the separate package was
tried and folded back.
What plugin management does now lives in `dsh-plugin-manager`, a boot-group
package beside `app-boot`: `PluginInstaller` runs pnpm, probes, and judges new
packages over the profile on disk, and `PluginManager` adds every operation
over the booted tree, each refusal a `PluginOperationError` with a `plugins/*`
code and no Remote protocol attached. The manager reads the profile runtime,
the preset roster, and the running-agent count through per-call readers, and
the roster only as `PresetLayers`, so the package depends on neither the
roster nor the agent registry.
`dsh-host-plugin-manager` keeps `pluginManager` and the `plugins` Remote as a
relay: one Remote method per manager method, and one exhaustive switch
turning a failure into the Remote error of the same code (`plugins/bad-request`
into `gateway/bad-request`). Its `./types` re-exports the manager's types, so
`api-remotes` and the client import one vocabulary unchanged.
`dsh plugin add` and `remove` now run the installer instead of a bare pnpm
forwarder: every new package is probed, a package that declares neither a
bundle nor a plugin module is removed again with its reason, and newly
installed bundles are enabled as before. Other pnpm verbs still forward.
The watermark marking and position comparison sit next to deriveEventMessage
in surface.ts; the append/seed validation sits with the other event validators
in index.ts and the fold is inline. Only compareImagePositions stays exported.
Remove the llm-image-offload package and every registration it needed. The
recovery branch lives in dsh-llm-retry, which already sits on the
agent/request-error waterfall: an IMAGE_OFFLOAD_REQUIRED failure advances the
image/offload watermark by the count the adapter names and retries the step
outside the provider retry budget. Drop pre-dispatch planning with it: routes
no longer declare a request-image budget on their model info, and the byte
checks that stay in the DeepSeek and pi-ai adapters are provisional until the
provider reports uncacheable images itself.
`dsh-global-tool-mask` wrapped `tools.restrict()` as a composition row so a
preset's user patch layer could hide a host tool. No shipped preset, the
plugin manager, and the settings UI compose such a row, so the package had a
README, a test, and no consumer. It goes, with its `apps/cli` dependency,
its `tsconfig.host.json` reference, the README example that named it, and the
generated catalogs. `tools.restrict()` stays: subagent tool filters call it.
Hiding a host tool from one preset is a per-preset setting for the
scope-resolved settings of the next layer, not a hand-written row; the Agent
Note records the removal under alternatives considered.
Keep the agent loop unchanged: the new @deepseek-ai/dsh-llm-image-offload
plugin plans the image/offload watermark on agent/pre-step from the routed
model's budget and advances it on agent/request-error when an adapter fails
with IMAGE_OFFLOAD_REQUIRED, returning the retry action the loop already
honours. Drop PreparedLlmCall.imageRequest and its validation, the planner
wrapper, the replay adapter's imageRequestMaxBytes option, and the duplicate
ACP snapshot scenario; share the exact-byte overflow count between the
DeepSeek and pi-ai adapters through requiredImageOffload(). The SDK image
snapshot replays an authored IMAGE_OFFLOAD_REQUIRED attempt instead of a
replay-only budget.
Second review round on the contained-group composition:
- Ownership scanned only `insert` rows, so an external bundle could set a
built-in row's id as a child through `{ id: own, config: [...] }` and the
Loader re-parented that row under the bundle while `originOf` still named
the built-in layer. The rows an id-targeted patch sets as a group's config
now count as the layer's own: an external bundle that sets an id another
layer owns is left out whole, a built-in layer that takes another layer's
id fails loud, and a layer restating its own child is not a repeat.
- `recompose` read the committed profile before awaiting the update, so a
watcher firing while a bundle was being enabled composed the tree before
the enable and applied it after; the plugin vanished while `layers` kept
listing it. Recompositions now queue, each reading what the previous one
committed, and a rejection stays its own caller's outcome.
- A patch that replaced a built-in target's config removed the wrapper the
bundle had put there, yet later inserts still targeted the detached
wrapper and vanished. The composition forgets the wrapper on such an
override and creates a new one under the same id.
- Generated wrapper ids bypassed the duplicate check, so a bundle declaring
a row spelled like a wrapper produced two nodes with one id. Declared and
generated ids now go through one registration.
- Failure records outlived their rows when a bundle update dropped a failed
row without unmounting the group, or when a pending row sat in a plain
nested group whose id the record named. A contained group now drops the
records of rows its configuration no longer names after each update, and
the audit records every row under the contained group that isolates it.
- `applyEntryPatches` assigned override values by reference, so applying a
patch list carried a later insert's rows back into the patch object; the
vendored include now clones object-valued overrides as it does inserts.
`applyEntryPatches` pushed a patch's `insert` rows into the entry list by
reference, so a later patch that inserted into a group an earlier patch had
inserted mutated the patch's own object. With bundle rows now inserted into
their group by a separate patch, the Loader's rollback of a rejected update
re-applied the previous include config over those mutated objects and hit
`duplicate loader entry id`; the rollback failed and the rejection reason
named it instead of the row that broke. Inserted rows are now cloned before
they join the list, as the function's detachment contract already stated;
the vendored change is logged and pinned by an application-twice test.
The publish guide still told bundle authors that mounted row ids carry the
package prefix and that `!!js` gates should compare names, a scheme this
stack withdrew: ids stay as declared, and a shared id or a repeated one
leaves the bundle out with the reason. It now says so, and names
`dsh.title` and `dsh.plugins` with a pointer to the manifest package that
declares every key. The cordis catalog follows app-boot's changed exports.
`stageOf` parsed the stage out of the Loader's `failed to <stage> loader
entry` message. The vendored wrapper is now an exported `EntryUpdateError`
carrying its `stage`, logged as a local modification, and the contained
group reads the field. `isJsDisabled` wrapped `isJsExpr` behind a literal
`disabled === true` comparison that already excluded expression nodes, so
it goes. The post-boot rejection guard, its documentation, and the
external-bundle module doc now say what they do: the process keeps running
with nothing stopped or attributed to a plugin, and a contained group lets
a failing row's siblings mount rather than failing the bundle as a whole.
The probe's child was a script string that wrote its report to stdout, and
the parent parsed the whole stream as one JSON value: a plugin that merely
printed a line at import could not be probed, and a package that kept a
timer alive after import ran until the timeout. The parent asserted the
parsed value as the report, and the cache read checked only `format`, so a
file holding nothing but `{ "format": 2 }` came back as a complete record.
The child is now its own module, `probe-child.ts`, run through tsx under a
source launch and bundled as `lib/probe-child.js`, and it sends one report
over an IPC channel; stdout is not read at all, and the child is killed as
soon as the report arrived. `parseChildReport` and `parseProbeRecord`
validate the report and the cached record field by field, so an
unrecognized report is a rejection and an unrecognized record is probed
again. `PluginProbe.ok` now states what it is: the main export imported and
cordis is not a second copy; `kind` and `addable[].ok` decide what can be
enabled or added.
`ProfileRuntime.recompose({ reloadBundles: true })` replaced its profile
before the Loader had accepted the update, so a rejected recomposition left
`current`, `layers`, and `originOf` describing a stack that never mounted.
The user patch watchers took a second path through the launcher's
`composeLive`, which wrote the new conflicts into the failure registry
before the update and kept composing from the runtime's stale profile.
Conflict records shared the registry with row failures under a fabricated
entry id and a group id that meant three different things, so the plugin
inventory attributed a conflict to the layer that won the id, and a bundle
disabled after failing kept its import and apply records forever.
The runtime now holds one committed composition — profile, stack ownership,
conflicts — and publishes a candidate only after `entry.update` resolves;
`originOf` reads the committed owners instead of recomputing them. The
watchers call `recompose` through a `reapply` callback, so every live change
goes through the one entry point. Conflicts live on the runtime with their
messages and never enter `pluginFailures`, whose records name rows that
reached the Loader; the inventory lists them under the layer that lost. A
contained group clears its rows' records when it unmounts.
`composeExternalLayer` inserted every id-targeted patch after all inserts,
so a bundle that replaced a group's config and then appended to it lost the
appended row under the `runtime` stage; two inserts into the same built-in
group produced two wrapper groups with one id, which the Loader rejected as
a duplicate and the boot failed; and an id a bundle inserted twice was
indexed once, escaped the ownership check, and failed the boot the same way.
The composition now inserts the bundle's group empty and emits the bundle's
patches in the order written, re-targeting root inserts into the group and
gathering every insert into one built-in group in one wrapper. A repeated id
is a conflict of the bundle with itself: the bundle is left out whole and
recorded like any other conflict, and a built-in layer that repeats an id
fails loud. `composeProfileStack` renders each contained layer once and
returns the owner of every id with the patches and conflicts, and one walker
in `patch-rows.ts` replaces the five recursive scans of inserted rows.
The client namespace service keeps install and remove for its own members
and refuses a Remote method of that name at mount, so the browser could
never reach plugins/install. A spec now reads the reserved names off the
gateway and checks every @Remote name against them.
After master's consolidation, the Manage plugins tab and the preset
detail's Capabilities section use Switch, Tag, Button, Input, and StateDot
from ui-primitives instead of their own copies; only the icon buttons, the
red trash icon, and the component chips stay local, as the catalog has no
answer for them. The three packages the stack adds take the workspace
version master released.
Only a failing component earns a line of its own; the ones a pack
switches off by design — base switches off dozens — sit as dashed chips
under why they are off, one group per reason.
A plugin pack's expanded card no longer lists every component as a row:
a count line says how many and how many are running, off, or failing;
only the components that are off or failing get a line, each with why;
the rest sit as chips behind Show all with a filter. base carries close
to a hundred rows, of which three are off on macOS — those three are the
ones worth reading.
The Delete on a row the person added to a preset is a red trash icon with
the same accessible name. The plugin card no longer says whether a running
session has the plugin loaded under the preset scope: the Host's registered
flag cannot tell "the preset does not compose it" from "no session runs",
so the line said nothing a person could act on. The unread shell field goes
with it; the README states when saved values apply instead.
Agent presets: every card that composes carries a gear that opens the
preset's detail page (breadcrumb, name, marks, description) and declares a
`settings.agentPreset.detail` slot for the sections other plugins contribute.
Plugin manager: the session-plugins group leaves the tab for the detail
page's Capabilities section, with a red Delete on rows the person added and
an Add menu over the installed modules the preset lacks; the manage tab now
precedes the configuration tab.
Plugin configuration: the scope switch is gone. The tab edits the values
shared by all presets, and the same cards form the detail page's Settings
section, registered under a second keyed slot
(`settings.agentPreset.plugin.item`) and editing the preset's own scope while
the page is open. The roster dependency the switch needed leaves with it.
Web e2e goldens, both READMEs of each package, the Agent Note, the slot
catalog, and the inspect catalog follow.
The Windows-only synchronous spawn-throw case still expected empty observed
readers, the behavior before a rejected spawn started serving its note as the
whole observed stderr stream. It now mirrors the bash-local case: the note is
the stream at every offset, stdout stays empty, and the consuming read folds
the note in exactly once.
Three review follow-ups on the consolidated jobs seam.
The spill file a pull source keeps is job metadata now: the pump reports it
to the registry on every read (present or withdrawn), the projection lists
it under output.spillPaths, and the model's dropped-output notice names it
from there. Attached to a gap chunk it was lost whenever the ring itself
evicted (a pump that kept up never produced a gap) or whenever the gap chunk
was evicted later, and it could not follow a source that withdrew its file.
A job_output wait's claim on the settlement now carries a live flag that
the caller's abort clears synchronously from the abort event. The registry
rejects an aborted wait on a later microtask, so a settlement announced in
between used to find the claim counted and skip the notice the tool result
was not going to carry either.
The job-controller client entry and the tool-jobs entry stop exporting
implementation classes and render helpers nothing outside the packages
consumes; the client entry keeps IJobs, which also carries the ctx.jobs
Context augmentation to consumers.
The caller-bound operation set now says what it is: the jobs one caller
may operate on. The caller argument is required (SessionId | undefined),
so an anonymous view has to be requested explicitly instead of falling
out of a forgotten argument.
Every installed package, built-in packs included, is one card in one list:
a name, a one-liner, and its switch or Add to… menu, tagged only for a
pending restart, a problem, or being built-in; kind tags, running and off
tags, the two helper sentences, and the Installed subtitle are gone, and
the toolbar button reads Add. Uninstall and retry move into the expanded
card beside the version, the source, and a pack's components. The
session-plugin composition uses the same single-column cards, marks a
plugin the person installed as Local, and tags nothing else but a failed
row. Tags take the preset section's pill shape.
The Manage plugins tab now speaks two nouns (plugin pack, plugin) and four
states (running, off, restart needed, problem). Cards carry a display name,
a one-liner, one kind tag, and their uninstall beside the switch; built-in
packs fold behind one line; the selected preset's composition renders as a
grid of cards named through the tab's own dictionary for harness modules
and through the manifest for third-party ones; entry ids, module names,
cordis copies, and probe times leave the page. The install dialog keeps
pnpm's output behind a fold that opens on failure and words each outcome;
confirmations name what still uses the package and no longer require an
acknowledgement. The client passes a preset row's declared id, not the
live tree's prefixed entry id, to the row actions.
The read-only Plugin list tab and its package, ui-settings-plugin-inventory,
are removed; the pluginInventory Remote stays as the manager's source of
preset compositions and host-tree module names.
The probe called any package whose main export is a function a `plugin`,
so `lodash` installed as an addable plugin module. A `plugin` now needs a
`dsh` section in its manifest or a dependency on `@deepseek-ai/cordis` as
well as a plugin-shaped export; a bare function export is a `library`.
The offer arm relayed the caller signal only through an abort listener, which a signal that was already aborted never fires, so a command cancelled before execute() spawned anyway, ran to its deadline, and was offered for promotion. The deadline callback also offered regardless of an earlier caller abort, so a process still in its termination grace could be promoted after the tool call was aborted. Both providers now relay an already-aborted signal immediately and settle the promotion undefined at the deadline once the caller aborted; the seam JSDoc states that an already-aborted signal counts as fired. Real-process and hanging-backend cases cover both providers.
The record mirror appended phase, log, and member lines without a channel, and the model's job_output renders channel-less chunks as stdout, so a job_output before settlement returned observer narration the README promised it would not. The four appends now carry channel: 'log', which the registry keeps out of model reads while observers still stream it; the spec pins the channels and the status-only early read.
ToolRuntime re-reads the caller signal immediately before execute(), and the workflow tool reaches jobs.start synchronously from there, so an in-tool check would be unreachable; bash and pwsh need theirs because they await a sandbox escalation approval before registering.
renderProcessRead and renderPwshProcessRead described themselves as the job_output renderer; their only caller is the timeout promotion embedding one consuming read, while job_output reads come from the registry ring. The pwsh presenter comment gains the promotions clause bash already has, and pwsh-sandbox drops a stray blank line.
The timeout-promotion, workflow run_in_background, and human-job-kill notes kept their bodies in the present tense over ctx.activities, RunningJob/updateDetail, and the registry's reported bit, with Update headers doing the correcting. Their mechanism sentences now state the shipped facts (observed streams as pull sources, JobHandle/updateProgress, the tool-jobs delivery ledger), the Update headers shrink to cross-links, and the testing paragraph names the current specs.
The tool-bash and tool-pwsh READMEs still said a promoted run is mirrored into ctx.activities, a seam the jobs consolidation deleted, and the shell README still called the observed readers optional and named the activity plane. The promoted run now feeds the registry's ring from the observed streams' current offsets and the Web list streams it through job.rows and job.observe; observed readers are required and carry the spawn-failure note.
The install dialog lists the packages the Host removed again after
`pnpm add` with the Host's reason, and every action renders the two
refusals any mutation can meet — another change still running, a session
still running — in the Host's words instead of a generic failure line.
A successful `pnpm add` is no longer the end of `plugins/install`: a new
package that declares neither a bundle nor a plugin module, or a bundle
whose row id a composed layer already owns, is removed again and reported
under `removed` with its reason, while a package the probe refused stays
for the view to explain. The profile manifest is snapshotted before the run
and restored when pnpm fails. The manager refuses a second mutation while
one runs (`plugins/busy`) instead of racing on the manifest and user
layers, and `install`/`uninstall` refuse while a session is running
(`plugins/agents-running`), because pnpm rewrites the directory those
sessions import from.
`resolveProfileLayer` factors one bundle's layer out of `loadProfile` so the
manager can judge a candidate against the composed layers.
The settled-retention JSDoc, the required observed streams, and the session-controller type shifts changed what the config, persistence, Cordis, and event catalogs embed; CI's static and coverage lanes compared the stale copies. Regenerated, with the zh mirrors updated in step.
A rejected background spawn produces no process output, so the observed stderr reader the job registry pumps was empty and the model's job_output after such a failure showed only killed before exit, where master's consuming read rendered [stderr] followed by spawn failed: … with the cause. The local providers now serve the note as the whole observed stderr stream once the spawn rejected, and the consuming read still folds it in exactly once, so the model's first read after the failure is byte-identical to master's. ShellProcess.observed and both of its readers are required: every background process exposes offset readers, and the shell tools' pull sources no longer carry a no-readers branch.
An owner's teardown settles and removes the record within one microtask chain, while the observation generation sleeps its flush window after the wake and then reads an id the registry no longer returns, failing the stream and flashing an error in the panel until the roster frame drops the row. The subscription now records a removed event for the observed id and the generation closes with that terminal projection.
The consuming shell read on master told the model where a truncated stream's complete text lived: [some output was dropped from memory; full output: <paths>]. The ring-backed read rendered a notice only for the ring's own eviction and ignored the gapBefore mark the pump puts on a chunk whose source lost bytes between two polls, so a fast producer's tail read as continuous output and no path survived. A lossy source read now carries the source's spillPath onto its gap chunk (JobSourceRead, JobAppendOptions, JobChunk), the pump forwards it, the ring retains it, and the model renderer emits master's notice for a producer-side gap or a ring eviction, listing the distinct spill files in offset order. The bash and pwsh sources already returned the subprocess reader's spillPath; the type now admits it.
The companion validated fields of single JobView projections, which the invariant rule rejects as a check without independent observations. It now tracks each job's announced protocol (registered first, one settlement, removal last, no progress after settlement), adopting a job first seen late, and compares every announced projection with the registry's own read of the same job: identity, output total, and the settled status and time. An output event announced before registration, which the registry emitted until the previous commit, is one of the cases it rejects.
An observation stream that failed before its opened frame left no observed state, so the expanded row rendered nothing: no output, no notice. The client model now records such a failure as an errored, non-streaming view with no resume cursor, and the panel renders the interruption notice it promises.
Two concurrent job_output waits on one job shared a single ledger entry, so the first wait's timeout withdrew the claim the second wait still held and the settlement injected a completion notice the model had already received through that wait. Claims are counted per call now; a withdraw releases only its own.
Settlement trimmed the ring to settledRetainBytes before releasing waiters, so a job that finished before its first job_output handed the model only the last 16 KiB plus a dropped-output notice where master's consuming read kept the executor's whole tail. The ring now keeps every byte the model cursor has not consumed on top of the settled cap, and the first terminal model read trims to the cap.
The registered event was emitted after startPump, whose first drain runs synchronously and may announce output, so a source readable at once produced an output event for an id no subscriber had seen registered. The announcement now precedes the pump; the events JSDoc states that a job's first event is registered.
Adopt owned row ids in the browser: the plugin manager tab shows `rowId`,
the web scaffold's profile runtime composes a stack, and the inventory spec
covers provenance and conflict attribution. Widen the install-result alias
the store spec had narrowed to `never[]`.
JobView.kind was typed as the merge-extensible JobKind, so the job-controller program, which compiles only the merges it can see, generated a strict codec of bash | subagent for job.rows and job.observe. The first pwsh, pty-send, or workflow job failed frame validation on both ends and the browser dropped the whole roster. The projection now carries kind as a string; JobSpec.kind keeps the Host-side JobKind. A built-artifact smoke parses a pty-send row through the generated schemas, and the seam spec pins the open type.
Adopt owned row ids in the plugin manager and inventory: a row view carries
`rowId` (the declared id a patch or row action targets) instead of the
withdrawn `originalId`, probe rows are listed under their declared ids, a
conflict record is attributed to the bundle it names, and the manager's
test boot records conflicts the way the launcher does.
The registry seam, runtime, admission, wake, and web display notes named JobStart, JobSnapshot, onJobDone, onJobsChanged, and the registry-owned reported bit as current facts. Rewrite those sentences to the shipped JobSpec/JobView contract, the events stream, and the tool-jobs delivery ledger; the wake note gains an Update header pointing at the consolidation note, and the display note's disposal paragraph now states the per-job removed event the registry emits.
Revert the two brace-only rewrites in tool-workflow to master's one-line form and collapse the blank lines left where SessionJob was removed from the session-controller types.
An external bundle keeps the ids its patch declares. composeProfileStack
decides ownership before anything mounts: built-in and boot-staged layers
claim first and a duplicate among them fails the boot, a contained bundle
that collides is left out whole, a user-layer insert of a taken id is
dropped, and every row left out is a `conflict` record in pluginFailures,
printed at boot and replaced on each recomposition. Boot, live reloads, and
--dump-config compose through the same function, so the dump shows the
contained groups the tree mounts.
The prefix made a user patch, a third-party manager's disabled rows, and a
bundle's own `!!js` id comparison miss silently; the note records both cases.
The Host announces plugins/changed while an install is still on the
wire — enabling recomposes before the call answers — and every such
event reads the packages again. The store used the read generation as
the install's liveness token, so the answer was dropped and the dialog
stayed at "installing"; the dependents answer of a confirmation was
dropped the same way. Liveness is now disposal, and for a confirmation
the confirmation still being the same one.
The probe compared the resolved cordis entry URLs of the child process
and the host. Under the source launcher tsx resolves the host's copy to
vendor/cordis/src/index.ts while the plain-Node child resolves
lib/index.js, so every third-party package was refused as carrying its
own copy. Compare the real package directories instead.
The install dialog now shows the Host's refusal reason after pnpm's
output when a run installs but cannot enable, and no longer forces a
width the modal cannot give it.
Add the Manage plugins tab (ui-settings-plugin-manager): package cards
with enable/disable switches, retry and uninstall, an install dialog
streaming pnpm output, and the selected preset's composition with per-row
switches and removal; the read-only list gains provenance facts.
Settings cards edit one scope at a time: ui-settings keeps one describe
mirror per settings scope (SettingsMirrorRegistry) and binds
{ namespace, scope }; ui-settings-plugins stages forms per scope, marks
inherited fields, and adds the skill-filesystem roots card.
Fixes surfaced by driving the browser end to end: the client gateway now
lets a call omit trailing parameters that accept undefined (PR3's
optional scope arguments failed the strict arity check), the plugins
install verb is `add` because the client namespace service reserves
`install`/`remove` (guarded by remote-method-names.host.spec), a plugin
module offers its main export as an addable `.`, and describing a settings
scope no owner registered resolves over the global instance's base and
never throws.
The web e2e scaffold can mount a profile runtime over fixture packages;
plugin-manager.e2e drives the tab and writes one field under a preset
scope; plugin-config and settings-chrome goldens are refreshed.
A settings namespace is now a kind; each registration is an instance
filed under the caller's nearest named scope (dsh-scope gains
createScope(..., { id }) and scopeIdOf). Instances resolve defaults →
composition base → global user section → scopes.<id>.<ns>. Global writes
fan out to every instance gated on its own resolved value; scoped writes
commit one instance. Revisions are per section so a scope nothing has
registered yet can still be written when the kind exists. describe(),
the controller verbs, and both events take an optional trailing scope.
Presets name their standing scopes preset/<id>; skill-filesystem is the
first consumer, resolving customSkillDirs per scope with live updates.
Add `dsh-host-plugin-manager`: the `pluginManager` service and the
`plugins` Remote that install, uninstall, enable, disable, and retry
bundles over the booted profile, add and remove rows in the profile's
or an agent preset's user layer, report what a package's services keep
alive, and fold the manifest, the probe record, and the live tree into
one view per package. pnpm runs the way the CLI runs it, streaming its
output as `plugins/install-log`; every change emits `plugins/changed`,
and api-remotes forwards both. Enabling is the Loader's own
transaction: a rejected recomposition restores the layer list, and a
runtime recomposition records contained rows through
`recordContainedStates` because the boot audit does not run again;
`ContainedGroup.create` now records a row that resolved pending rather
than clearing it.
Give every agent preset a user patch layer: `cordis.patch.yml` beside
a locally authored composition, or alone in `.agent-presets/<id>/`
for a shipped one. Discovery attaches and judges it, the mount applies
it as the include's runtime patches, standing generations follow the
layer's content and return to a retired generation when it comes back,
the composition inventory reports each row's `source` and
`disabledBy`, a copy carries the layer, and `removeOverlay` deletes it.
`dsh-global-tool-mask` is `tools.restrict()` as a scope-only row for
such a layer.
Add `dsh-patch-file` as the one home of the patch-list format: the
parser moved out of app-boot, plus a comment-preserving key-level
writer with lock, atomic replace, and readback. app-boot exports
`layerTrust`, `bundleLayerPatches`, and `recordContainedStates`, and
`ProfileRuntime` carries the install anchor.
Mount every external bundle layer as a cordis:contained-group entry
`bundle/<package>` whose rows carry a `<package>/<id>` prefix, so one
community bundle that no longer loads records a failure in the root
`pluginFailures` registry instead of rejecting the whole Loader
transaction, and two bundles may declare the same row id. Classify
layers by origin (`builtin` for the template bundle and
`dsh.profile.firstParty`, `external` for profile dependencies) and let
`dsh.bundle.stage: boot` or `dsh.profile.stages` opt a bundle out of
isolation when it provides a service built-in rows inject; a built-in
row left pending beside an isolated bundle still fails boot and names
the bundle.
Keep `installFailLoud` for the boot transaction only and install
runtime guards afterwards: an unhandled rejection is reported and
contained, an uncaught exception is reported and exits. Report nested
fiber failures under built-in entries as advisory lines.
Provide `ctx.profileRuntime` from the launcher (layers with trust and
stage, row origin, user-disabled rows, recomposition through the root
include) and probe installed packages in a child process
(`probePackage`, cached under `profiles/<name>/.dsh-plugins/`). Split
installation from enablement in `reconcileInstalledBundles`; the CLI
keeps install-means-enable. The plugin inventory now reports `trust`,
`package`, `disabledBy`, and `failure` per row and lists rows only the
failure registry knows.
With owners resolved by session id, the suite's id-less fake agent no
longer makes the timeout promotion fail admission, so a per-call timeout
promoted the run and the timed-out-marker assertion broke. Promotion over
a scripted executor stays covered by background.spec.
The merged tree carries promoteOnTimeout, the workflow run_in_background
config, and tool-jobs' pre-refactor requirement set; the settled job
goldens show the folded finished section, a static row for a job with no
retained output, and the human kill's reason in the detail.
An owned job cannot start without the agent registry, so the plugin only
needs it to resolve a notice destination; requiring it left compositions
that run unowned jobs — the PTC harness, the pwsh loader and integration
suites — without an attached controller. The inject list, catalogs, and
README return to the pre-refactor requirement set.
A settled job that retained no output is a static row, and a killed row
shows the kill reason in its detail. The live-stream scenario's idle
locator now matches the settled trigger exactly, so its settled capture
cannot race the roster refresh.
The rewrite carried an older wording of the system-prompt section; the
recorded-session snapshots pin the current one. Model-visible text is
byte-identical to the pre-refactor plugin again.
The notice and its summary passed an optional detail as string | undefined,
which the build's exactOptionalPropertyTypes rejects while the test lane's
transform does not; publicJob() already carries the conditional.
- docs/subsystems/jobs.md (+zh) records JobSpec, JobHandle, JobHooks,
JobOutcome, the pull-source types, JobChunk, JobOutputRead, JobView,
JobRead, VisibleJobs, and the event types as type-equivalence blocks; the
manifest and the cordis catalog link map follow
- the jobs, jobs-local, and tool-jobs READMEs (+zh) describe the ring, the
caller-bound view, the event stream, and the notice ledger; the
architecture table and the adding-a-tool cookbook drop record: true and
readOutput
- Agent Note 2026-09-03-jobs-seam-consolidation owns the decision; the
2026-09-01 record note and the 2026-08-08 web display note carry
Superseded pointers and the display note links the rows spec
- regenerated catalogs (cordis, api, inspect, client, tool, config,
persistence, doc graphs, module graph) with their Chinese mirrors; the
cordis catalog drops the jobOutput exemption and the tool catalog mounts
the agent registry for tool-jobs; the client model imports JobView from
its leaf so the typert analyzer resolves it
The header list watches its session's roster through ctx.jobs.watchRows
while mounted and reads rows and observed views from one jobs snapshot. A
row is expandable while the job is live or once it left retained output
behind; it shows the live progress line or the terminal detail. The web
e2e drivers kill through visibleTo(), and the client fixtures the roster
removal missed drop their jobs baselines.
- job.rows({ sessionId }) streams the jobs one session can see as whole-set
frames: once on open, then after each coalesced burst of lifecycle
commits; output appends never refresh it
- job.observe reads through visibleTo(sessionId) and carries JobView in its
opened and status frames; the wire chunk is the registry's JobChunk
- the client installs ctx.jobs (ClientJobs over ClientJobsModel): watchRows
and observe are reference-counted streams, rows drop with the last
watcher or a terminal failure, observations keep their render tail
- the controller no longer needs the agent registry
The control baseline and frames carry queues and projections only; the
SessionJob row type, the jobs frame, the registry subscription, and the
client jobsBySession mirror are gone, and every client fixture drops the
empty mirror. The roster moves to dsh-api-job-controller's job.rows
stream in the next change.
- tool-subagent registers the parent session id as the owner and settleRun
reports the child's final text as the job result, which the first read
after settlement carries once
- tool-terminal hands the registry one pull source over the backend's
consuming send reader (sendSource in background.ts) instead of a
readOutput hook; the truncation marker stays in the delivered text
- the model's kill reason now shows in a killed job's status line, as the
registry records it as the terminal detail
- test call sites move from jobs.list(owner) to visibleTo(owner).list()
tool-bash and tool-pwsh no longer render their own background reads.
processSources() wraps the process's observed offset readers as registry
pull sources, pumped at dsh-jobs-local's pumpPollMs, and processOutcome()
folds the settled sandbox facts into the terminal detail. observe.ts and
the recordPollMs config field are gone; READMEs and pairings follow.
- job_output reads the per-job ring through visibleTo(): stdout, then one
[stderr] section, log narration excluded, a dropped-output notice on a
lossy read, and the result exactly once after settlement
- completion notices come from the registry event stream subscribed with
{ owners: 'scope' }; a wait claims its job when it starts and withdraws
on timeout or abort, job_kill claims on 'requested', teardown settlements
and unowned jobs are skipped, and a removal drops the ledger entry
- the notice destination is the agent registered for the owner session at
settlement, so tool-jobs now injects the agent registry
- JobView exposes the producer's outputLimitBytes for the tool-result bound
- lint: hub disposer identity comment, unknown-typed matchers in seam tests
Record the image offload point as the core image/offload session event so the
derived surface, every adapter, and the token meter share one offloaded set.
The agent loop plans the advance from the route's declared request-image
budget before dispatch; an adapter whose exact accounting still overflows
fails with IMAGE_OFFLOAD_REQUIRED and the loop advances by the named count.
Every job owns an `OutputRing`; the model's `read` consumes from a
registry-kept cursor while `readAt` reads at absolute offsets. Pull
sources named by the spec are pumped by the registry and drained once
more before settlement. `visibleTo(caller)` binds the session fence
once; the three listener families become one event hub with explicit
filters; `reported` is gone and settlements announce their cause.
The registry seam keeps three members plus the controller handshake:
`start(spec)`, `visibleTo(caller)` (one authorization, then list / get /
read / readAt / kill / wait without a caller argument), and `events`
(one typed stream with an explicit filter instead of three listeners).
Every job owns an output ring: `JobSpec.output` names pull sources the
registry pumps, `JobHandle.append` pushes, and the model's consuming
`read` and observers' `readAt` read the same bytes through separate
cursors. `reported`, the `record` declaration, the producer `readOutput`
hook, and the `JobStart`/`RunningJob` split are gone; `JobView` is the
one projection, split into `progress` and terminal `detail`. The
client-safe `./view` leaf carries the projection vocabulary.
- `JobStart` is a union discriminated by `record`: `RecordingJobStart.run`
receives `RecordingJob` (with `append`), `PlainJobStart.run` receives the
plain `RunningJob`, and the registry builds the plain face without the
method, so the former warn-and-drop path is unrepresentable.
- Remove the duplicated JSDoc opener in the registry seam and correct the
job-list slot comment (order 20 sits before the subagent catalog's 30).
- Restore the `cordis-tool-round` session fixture whose only change was a
re-recorded `createdAt`.
- Update the 2026-08-08 job-display note's stale service-disposal and slot
ordering facts; archive the superseded 2026-08-25 unified-task-list note
and point its remaining inbound links at the archived snapshot.
Review corrections on the observation record:
- `pumpJobOutput` subscribes to `done` once and keeps one pending timer;
racing the same pending promise every poll round retained a reaction
per round for the life of the job.
- The `ctx.jobOutput` effect disposer is async and awaits every open
carrier's `dispose()`, so the fiber reports unloaded only once the old
iterators are quiescent.
- `SessionJob.record: true` replaces `outputTotal`: `append` never commits
a roster change, so the mirrored count was stale while output flowed;
the live offsets ride the observation stream's `opened` anchor.
- The record is documented as a best-effort live preview whose
cross-stream reordering is bounded by the poll interval.
- The web e2e scenarios hold their background job on a test-owned barrier
file instead of a wall-clock sleep, and each is one test rather than a
chain sharing a job id.
`session.killJob` becomes `job.kill` on `dsh-api-job-controller`, beside
`job.observe`, so the Session Controller carries only session control and
the roster while every per-job Remote lives on the job namespace. The
client `ctx.jobOutput` gains `kill(sessionId, jobId)`; `dsh-client-ui-jobs`
routes its stop control through it and no longer requires `sessions`.
Unknown or foreign jobs reject with `job/not-found`; the controller
requires `ctx.jobs` to load, so `jobs-unavailable` disappears. The two
subagent ownership-fence helpers are classified duplicate-install-safe
Host exports of the Session Controller for the cross-controller import.
Review asked for the observation wire and its client service to leave
session-controller: a job should reach the Host only through its own
Remote namespace, and the session plugin's apply had to materialize
{ $stream, session } for a service that is not session state.
@deepseek-ai/dsh-api-job-controller now owns the Host JobController
(job.observe over ctx.jobs, requiring the agent and job registries) and
the client ClientJobOutput (ctx.jobOutput) over ClientJobOutputModel,
resolving both Remote faces in its own apply. The remotes assembly
mounts the job namespace; ui-jobs injects the new package; the wire
types drop their Session prefix and spell the status union out so the
client face pulls no Host declaration merges into browser programs.
session-controller keeps the jobsBySession roster and loses observe-job,
job-output, and the observe cadence config.
ui-chat's slots and snapshot-builder import CommandNode and friends from
the declaring package: the typert analyzer's cross-package rule trips on
the snapshot re-export as soon as another client face joins the root
Client program (the new controller does), which is why this branch
carried the change before. JobPumpSource documents why it is an offset
snapshot reader rather than a Node.js Readable.
Carries the session seq-brand master sync forward; unions the seq/offset
imports with this branch's jobs augmentation in commands.ts and
regenerates the catalogs and zh ports over the merged source.
Absorbs the session seq-brand refactor and the turn-rail preview layer;
regenerates the catalogs and graph docs from the merged source and
re-ports the zh config/event rows this branch owns.
Regenerates the module graph for tool-workflow's new dsh-jobs edge (the
doc-sync aggregate does not run that gate), refreshes the web lane's own
schema/prompt pins for the run_in_background tool text, re-packs every
session fixture the keyless refresh had left off the canonical packed
layout, covers pwsh-local's never-spawned empty observed readers, and
rewrites the pwsh-sandbox sync-rejection start() test to the seam's
actual containment contract — execute() never throws; the sync spawn
throw settles the handle killed and result() carries the attribution
split. That test sits behind the pwsh probe, so only the Windows lane
had ever executed it.
The workflow tool gains run_in_background (enableRunInBackground config,
default on): the run registers on ctx.jobs as an owned 'workflow' record
job and the call returns { kind: 'background', jobId, runId } immediately.
The run belongs to the job — job_kill, the list's stop control, and owner
teardown cancel it; no tool-step signal is bridged. Settlement maps the
stop reason onto the job outcome, with a completed run's rendered return
value riding output to the completion notice and job_output. A record
mirror streams workflow/phase, workflow/log, and member lifecycle lines
into the job record, replacing the removed activity mirror; the output
schema becomes a kind-discriminated union and 'workflow' joins JobKindMap.
Snapshot expected outputs are refreshed for the schema, prompt, and PTC
stub changes (the keyless refresh also migrates fixtures to the current
session write format: range sourceEventSeqs and reasoning-delta chunks).
The promotion machinery was copied from bash-local without its tests;
runners without pwsh skip and exempt the file, so only the Windows
coverage lane surfaced the gap.
An agent-attributed timeout promotion fences the job to the calling
session in both shell tools, and the pwsh promoted job's cancel hook is
exercised through a registry kill.
Covers the woken-wait fast path (a wake landing while the generator is
suspended on a yield), the opened frame's live-detail arm, and the
sessionId-less caller resolution over an unowned job.
Carries the hairline-elevation sweep forward; the settled-section heading,
chevron box, and stop control move to 0.5px borders while keeping this
branch's padding.
Absorbs the superellipse-corner and hairline-elevation sweep; keeps the
data-with-body header divider on TerminalBlock at the new 0.5px width and
brings the job-list settled-section rule onto the hairline standard.
Carries the master sync forward (invariant-companion cleanup, alpha.3 bump);
regenerates the client api catalog so ISession lists both killJob and the new
loadThrough, and refreshes the new ptc-python-turn scenario's system-prompt
pin for this branch's bash timeout-promotion schema text.
Absorbs the repo-wide no-op invariant companion removal (ui-jobs loses its
placeholder companion, export, and devDep), the 0.1.2-alpha.3 release bump,
and the tool-subagent-report package retirement; regenerates the client slot
catalog for the shifted contract line numbers.
Catalog line references and the re-landed promoteOnTimeout blocks follow
the merge in both languages; the ordering test narrows getAttribute's null
before startsWith so the client face compiles.
Observation (re)opens run on caller stacks — a React expand click, a
carrier retry — whose dynamic context has not declared remote.session, so
the ClientJobOutput wiring resolves the namespace to a concrete value while
the plugin's own context is current, exactly as the activity client did.
Caught by the live-job-stream web e2e, whose goldens (with the
background-job-list pair) now carry the ui-jobs list name; the client-apply
spec gains a jobOutput wiring smoke.
The regenerated session namespace requires the new stream; the fake serves
a never-yielding generator, and job-output.client.spec exercises the real
consumer.
Chinese sides of the jobs, jobs-local, tool-bash, tool-pwsh, tool-workflow,
session-controller, and ui-jobs pages follow their English edits; the
generated en-only catalogs (config catalog, capability seams, event
producer/consumer) get the same section surgery by hand; the activity
subsystem page triplet is fully removed and the jobs page carries the
record contract in both languages with fresh type-equiv manifest entries.
The three prior activity notes point at the superseding note. Web e2e label
assertions follow the ui-jobs copy, and the three test call sites that
invoked run() bare pass the producer face.
packages/activity/* and packages/api/activity-controller are removed. The
web roster is the session control stream's jobs frames alone; per-job output
observation rides session.observeJob and the client ctx.jobOutput service.
ui-activity returns to its upstream name ui-jobs and renders the single
roster — a row is expandable exactly when its job declared a record
(outputTotal present). Composition rows, dependency-policy exemptions,
tsconfig aliases and references, generated catalogs, and the docs pages
follow; the activity subsystem page is deleted and the jobs page gains the
record contract. The superseding Agent Note records the decision and points
the three prior notes at it.
SessionJob rows carry outputTotal exactly for record-declaring jobs, and a
new session.observeJob stream serves one job's retained record from an
absolute offset (anchor, coalesced output frames, terminal status) with the
fenced read resolved from the request's session — the same authorization
shape as the jobs roster. The client half gains ctx.jobOutput: the
reference-counted per-job observation service and its accumulated view
model, ported from the activity controller this replaces.
bash and pwsh background runs declare record: true and pump their observed
streams straight into the job through observeProcessRecord — the open/end
pairing, outcome mapping, and registry-absent degradation branches are gone,
and the pump's final drain folds into hooks.done so the record holds its
last bytes before settlement closes it. activityPollMs becomes recordPollMs.
tool-workflow drops its foreground activity mirror deliberately: a
foreground run's live phase/log narration had the mirror as its only
carrier, and the transcript keeps the recorded run/member lifecycle events.
Live narration returns with backgrounded runs when workflow gains
run_in_background.
A job can now declare record: true and stream raw output into a bounded
per-job ring through the RunningJob face its starter receives: readRecord
serves any number of observers at absolute byte offsets without touching
the consuming model cursor or notice state, onOutput signals advancement,
and updateDetail publishes a live progress line into every snapshot. Job
settlement is the record's only end: it trims retention to the settled cap
and fires the final output signal, so producers no longer reconcile two
terminal states. pumpJobOutput ports the offset-reader pump beside the
seam. This is the first step of merging the standalone dsh-activity
observation plane into jobs.
ActivityListAction imports dsh-session types; the flattened client-face
policy wants that relationship recorded as a devDependency, and the
peer-slimming merge round dropped the entry.
- tool-subagent's two job_kill scenarios now see the model's kill reason
in the killed detail — the registry has merged kill reasons into the
killed outcome since the human-kill work, and the master merge routed
subagent aborts through that same settle path; the specs pin the new
text.
- 112 session fixtures re-recorded by the earlier snapshot refresh
predate master's canonical packed layout; migrate:packed-session-fixtures
rewrote them mechanically and the keyless replay stays green.
- ActivityListAction's clear-while-expanded arm gets its missing client
spec case (per-file coverage gate).
The master merge brought the published dependency policy: a package
declaring dsh.client flattens its non-Cordis peers unless excluded.
api-activity-controller is the same species as the excluded session and
workspace controllers — a Host Remote controller whose host half relays
real runtime peers (dsh-activity for the registry service and its brand
constructor at the wire boundary, dsh-invariants for the companion) —
so it joins CLIENT_FACE_EXCLUDE rather than dropping those peers.
The master merge slimmed the client Session constructor to (id, remote,
options); the killJob specs from this branch still passed the FakeApiClient
as a second argument and stopped compiling.
The coverage lanes flag four uncovered locations after the master merge:
the spec.owner spread in tool-bash/tool-pwsh observe, the workflow mirror's
owner spread, and tool-pwsh's outer observation catch. The covering tests
existed only on the stacked job-kill branch; give this branch its own:
an owned background run per shell asserting the fenced ownerSession row,
the owned workflow mirror run, and a throwing open() through the pwsh
outer catch. The windows-only file-reference-local gap reproduces on
master's own windows lane at the merge point and stays upstream.
- Map an absent persistence backend to queue-item-not-found for updateQueue.
- Re-fence the published Agent after shared cold resume settles.
- Document cold queue activation and regenerate its public API catalog.
- Supersede the attached-only claim in the Web subagent conversations note.
Review of the first hierarchy pass in the browser: the 26px bordered controls
read oversized and the card fill visually stopped short of the stop button.
The controls drop to 20px squares, the live card's inner padding now frames
the dot and both controls on the fill, and the hover wash moves onto the card
itself so nothing escapes it. A live row's second line becomes kind + ticking
duration (the redundant 'running' word only returns through a real detail).
The settled tail folds behind a 'Finished N' toggle while live work exists
(auto-expanded otherwise) with a client-side Clear that hides the current
tail without touching the registry; goldens re-recorded for the new tree.
Live rows read as filled cards (fill-l2, the kind badge pops on the menu
surface instead), the expand chevron and the stop control become persistent
bordered squares (the stop turns danger on hover), and the popover widens to
500px. The settled tail keeps its flat rows, so running work now outranks it
visually — previously the colored settled dots were the loudest thing in the
list and both controls were bare tertiary glyphs invisible before hover.
The armed state relied on a color swap through --dsw-alias-label-error, a
token the themes never define, so the confirm step was effectively invisible.
The first press now widens the button into a labeled danger pill (确认停止 /
Confirm stop) tinted with the real state-error token StateDot uses; the
stream-failure notice moves off the same phantom token. Component spec pins
the inline label.
- session.killJob maps job-not-found from the lookup only: jobs.get gates the
same synchronous span, so a producer-cancel throw now propagates per the
registry contract (job state unchanged) instead of masquerading as a lookup
failure (threads r3860226021, r3860226047).
- session.killJob applies the service-wide subagent ownership fence exactly as
cancel does; a subagent-owned live session rejects with the ownership error
and the child-owned job stays running (thread r3860226041).
- The stop control stays pending on an admitted kill: the unary response and
the jobs frames have no cross-carrier ordering, so only the authoritative
frame (the row leaving the killable set) releases the button, closing the
duplicate-kill window (thread r3860226028).
- One-line comment on the killReason assignment stating last-writer-wins is
intended, beside the first-claim-wins reported handling (thread r3860225999).
Host spec pins the producer-throw propagation and the fence; the component
spec pins pending-until-frame; the Agent Note records both refinements.
The executor seam collapses run()/start() (and the planned begin()) into one
resolve() + execute(): the handle IS the live process, and the foreground
result, the background cursor, and the deadline's promotion offer are
projections over the same spawn. ShellExecSpec.onExpiry ('kill' default /
'offer' / 'none') replaces the implicit method split; one stdout budget applies
everywhere, the 'start ignores timeoutMs' wart dies, and sync spawn throws are
contained into the same settled-killed handle as async rejections (result()
rejects with the original error, identity preserved for the sandbox
classification).
The promotion offer settles exactly once and never rejects — offer at an
'offer' deadline while running, undefined when the process settles first — so
'await execution.promotion' alone distinguishes the outcomes. An offer must be
answered synchronously; an unanswered one auto-declines back to the
kill-on-timeout behavior. accept() ends the deadline and detaches the caller's
abort signal; decline() kills and classifies timedOut.
tool-bash and tool-pwsh promote on timeout by default (promoteOnTimeout: true,
inert without run_in_background or a job registry): at the offer the running
handle registers as a job with the same hooks as run_in_background, mirrors
into ctx.activities (the web task list streams it, stop control included), and
the call returns { kind: 'promoted', jobId, timeoutMs, output } carrying the
output so far — the job cursor continues exactly after it. Any promotion
failure declines and falls back to the plain timeout kill. Descriptions and
the timeoutMs schema text advertise the semantics only when they hold;
recorded-session and web snapshot fixtures re-recorded with them.
Also repairs the four CI failures from the human-kill push: ui-activity's
missing dsh-session peer declaration (static + windows observational), the
observe owner-arm/outer-catch and killJob-stub coverage gaps, tool-workflow's
owned-run arm, and the timing-dependent projection-cache interval flush test
(now poll-based for coarse Windows timers).
Peer evidence and rejected alternatives (Codex's session model, a third seam
method, a second threshold knob) in the Agent Note
2026-08-26-shell-execute-timeout-promotion.
The web task list gains a two-press stop control on running job rows, built on
one seam change: JobRegistry.kill now takes { reason?, reported? } instead of a
positional reason. reported defaults to true (the model's job_kill claims the
terminal report exactly as before); the web path passes reported: false because
a human kill has no model-visible channel, so the settlement flows through the
existing tool-jobs completion notice (wakeup/inject, budgets, truncation all
unchanged). A recorded kill reason merges into a killed settlement's detail
(producer facts first: 'signal: SIGTERM; cancelled by the user'), so the notice
and the web row both say who stopped the work.
- session-controller: session.killJob Remote beside cancel, live-only Agent
lookup, job-not-found/jobs-unavailable rejections joined to the closed RPC
error table; ClientSession.killJob and the ISession face carry it to the UI.
- ui-activity: arm/confirm stop button (3s disarm, pending disable, failure
hint), rendered only on running job rows; standalone activities keep no kill
handle and the README limitation now says so.
- The background-job-list web e2e now drives the whole human path through the
button; goldens show the merged killed detail end to end.
- Agent Note 2026-08-26-human-job-kill records the decision against Kimi's
suppress-own-notification precedent and Claude Code's silent-kill failure;
the 2026-08-08 blocker note and the unified-task-list note point at it.
- live-activity-stream.e2e.ts still seeded from the old
apps/web/tests/snapshots path and navigated to the unauthenticated
baseUrl; the merge moved session recordings to snapshots/web/ and put
browser trust behind scaffold.authenticatedUrl, and both updates reached
the pre-existing background-job lane but not this branch-new file.
- tool-bash/tool-pwsh observe: the settlement-mapping catch introduced by
the review fix had no covering test; both specs now drive a throwing
end() through it.
- useDismissOnOutsidePointer lost its only branch coverage when the
ui-jobs suite was deleted; the hook now has its own unit spec (outside
close, inside ignore, closed detach, non-Node target).
- THIRD_PARTY_NOTICES.md regenerated at the merged lockfile's
claude-agent-sdk 0.3.241: stale 0.3.220 store directories made the local
generator resolve the old payload, so the committed file disagreed with
a clean install.
Both ds-review-bot criticals verified and fixed:
- ClientActivityFeed release closures resolved the entries map at release
time, not the entry they were minted for. A late release of a superseded
observation decremented (and could tear down) the successor's stream, and
the post-dispose model clear could land after a re-observation inside the
dispose round-trip re-anchored the view — blanking the panel until the
popover reopened (React StrictMode's double effect hits the same window).
Releasers now capture their entry, reconcile the map only when it still
holds that entry, and skip the view clear whenever a successor owns the
key. Both regressions fail on the previous code.
- The background observation tap mapped the process outcome as soon as the
pump settled — including its failure path, where the process is usually
still running: processOutcome requires a settled handle, so an early
mapping froze 'exit code: 0' onto a live task and first-wins end discarded
the real terminal state. The tap now awaits proc.done before mapping (the
promise never rejects) and swallows mapping failures per the module's
contract, in bash and its pwsh mirror.
Also applied from the same review: the Remote tier's cross-session
visibility is now stated in the controller README, ActivityRead.next
documents its chunk-boundary nature that client concatenation relies on,
mergeRows records the one-activity-per-job assumption, and the 08-24 Agent
Note now reflects the shipped unified task list.
The TerminalBlock banner divider used to key off the running flag from the
days when a running card was banner-only; a running card that streams live
output now needs the same separation, so the divider follows the rendered
body (data-with-body) instead. The task list labels each non-empty section
(Running / Finished) rather than only dividing the settled tail, and the
leading section drops the divider line above its heading.
Interactive acceptance produced five presentation decisions:
- Header band order is preset → tasks → subagent catalog. The root-session
descendant count moves out of the breadcrumb (header.lineage) into the
actions band (order 30) as SubagentCatalogAction and drops its '/'
separator — a root session has no hierarchy to breadcrumb. Child
sessions keep their root/child switcher unchanged.
- The panel's copy control copies the command (TerminalBlock.copyText),
not the output: a long command ellipsizes in the row, so the control is
its only complete source, and it stays rendered before any output.
- Commands and output lines wrap in full (--dsl-terminal-command-whitespace
/ --dsl-terminal-line-whitespace: pre-wrap), so the output region never
scrolls sideways; the region caps at a fixed height and scrolls
(--dsl-terminal-output-max-height) with maxLines: Infinity disabling the
head/tail fold.
- The panel draws no run-state dot (TerminalBlock.runStateDot: false) —
the row above carries the same state — and reclaims the dot gutter via
--dsl-terminal-gutter, which must target the card element itself because
the variable is declared on it. The panel's left spine is gone with it.
All TerminalBlock changes are opt-ins (props or --dsl-terminal-* variables);
tool cards keep their shipped fold/ellipsis/dot behavior.
Browser acceptance of the activity panel surfaced the product problem: a
background bash registers in both planes, so the header grew two near-
identical popovers — "background jobs" with lifecycle and duration,
"activities" with live output — listing the same work twice.
The service planes stay orthogonal (jobs: model-visible control with a
consuming read cursor; activities: transient non-consuming observation).
The join happens per row in the UI projection instead:
- dsh-client-ui-activity renders the single merged "tasks" list. Each
jobsBySession row joins the activity carrying its correlation.jobId —
the job supplies identity, lifecycle (stopping exists only there),
duration, and the model-visible detail; the activity supplies the
expandable live-output panel. Standalone activities (workflow runs)
keep their own rows; without the activity registry, job rows still
render and the control degrades to the old job list exactly.
- dsh-client-ui-jobs is deleted (pre-release, no shim); the merged entry
takes its slot order. The job locale namespace folds into activity.
- User-facing copy says "task" (任务): N tasks running / N 个任务进行中.
- Hierarchy: live rows first as two lines (label over kind badge and
status, ticking duration), a Finished divider when both sections
exist, settled rows compacted to one de-emphasized line, and the
output panel hangs behind a spine under its row.
Deliberately not joined: subagent rows stay bare (the subagent panel is
their surface) and foreground commands stay in tool cards.
Both web e2e scenarios now assert one Tasks list; the background-job
scenario's row gained a live-output panel, proving the join end to end.
Agent Note: 2026-08-25-unified-task-list.
Browser acceptance against a live scaffold surfaced two defects:
- A fresh observation anchors at the registry's earliest retained byte, so
the client's gap predicate (anchor behind earliest) never fired for it: a
viewer who watched eviction live saw the gap notice, while a reload of the
same activity silently hid that the head was discarded. A fresh view (no
accumulated text) anchored past offset zero now carries the gap mark;
resumes keep their recorded state.
- The trigger-anchored popover (left: 0, width 440px) overflowed the right
viewport edge on narrow windows — max-width alone cannot see the anchor
offset. The open popover now measures its anchor and shifts left to keep
the Menu primitive's 12px viewport margin, re-fitting on window resize and
falling back to the pure CSS anchor when layout reports no width (jsdom).
JobListAction shares the latent overflow; tracked separately.
Add the activity capability seam: ctx.activities is a pure observation
plane where long-running work (background bash/pwsh, workflow runs) opens a
handle, appends transient output into a byte-offset ring buffer, and settles
exactly once. Nothing model-visible changes: reads are non-consuming and
lossy-by-design (absolute UTF-8 offsets survive eviction and report gaps),
correlation carries {callId, jobId}, and visibility is owner-fenced like jobs.
The seam is strictly optional. Producers tap it through ctx.get('activities')
and contain every observation failure, so an unregistered or broken registry
never affects the underlying job (proven by broken-registry tests).
Ship the full path in one change:
- @deepseek-ai/dsh-activity: Service Definition, pumpActivityOutput pull
substrate, invariant companion; dsh-activity-local: ring-buffer provider
with retention config and owner cleanup.
- shell: ShellProcess.observed exposes non-consuming stdout/stderr readers;
tool-bash/tool-pwsh mirror background jobs into activities.
- tool-workflow: phase/log/agent lines mirrored as a workflow activity.
- api-activity-controller: Typert Remote control (roster baseline+buckets)
and observe (opened/output/status frames, coalesced and byte-capped)
streams plus the client feed service with reference-counted observation.
- ui-activity: session-header action rendering live output through
TerminalBlock, which now shows output while a command is still running.
Docs: activity subsystem page, architecture row, regenerated catalogs, and
the Agent Note (2026-08-24-activity-observation-seam), all bilingual. A
keyless web e2e replays a real run_in_background stream to ARIA goldens.
Adding another client-face project to the client aggregate reshuffles the
typert analyzer's batches, which exposed latent provenance violations in
ui-chat and ui-approval: types consumed through ui-conversation's re-export
path instead of their declaring package, and one slot-contract literal whose
type the analyzer could not name. Import the types from
@deepseek-ai/dsh-client-ui-conversation/client directly and give the approval
interaction kind a named alias so the explicit annotation survives both the
analyzer and lint.
@@ -16,7 +16,7 @@ Every Agent Note has two axes, both encoded in its **path** — `{lifecycle}/{cl
The date in the filename is when the topic was **first proposed** (per git history). Cross-references between Agent Notes use relative markdown links (`[topic](../../implemented/architecture/2026-…-….md)`) — never bare prose or numbers — so they are mechanically checkable and survive moves between folders.
The active lifecycle tree is the working inventory: browse its lifecycle/class folders or search the repository. Do not add a centralized `INDEX.md`; the [no-index Agent Note](implemented/process/2026-07-19-remove-generated-agent-note-index.md) owns the rationale. Low-future-value implemented records move to the separate frozen [`archived/`](archived/AGENTS.md) tree described below.
The active lifecycle tree is the working inventory: browse its lifecycle/class folders or search the repository. Do not add a centralized `INDEX.md`; the [no-index Agent Note](implemented/process/2026-07-19-remove-generated-agent-note-index.md) owns the rationale. Implemented records with historical decision value but little future guidance move to the separate frozen [`archived/`](archived/AGENTS.md) tree described below.
## Classification
@@ -35,17 +35,17 @@ The `architecture` / `process` line: **architecture** is about the source we shi
## Archiving and deletion
Archive an implemented Agent Note when the shipped decision is complete and its rationale is unlikely to guide future work. Keep it active when its alternatives, ownership boundary, negative guarantee, durable or wire semantics, security rule, or reintroduction condition remains useful. Never archive a proposed note: reject an obsolete proposal. Keep a rejected note only while it prevents a plausible mistake; otherwise delete its English, Chinese, and sidecar files together. Use the calibrated [`dsh-archive-agent-notes`](../skills/dsh-archive-agent-notes/SKILL.md) workflow rather than word count, age, or a target quota.
Delete an implemented Agent Note that only describes small UI adjustments or purely mechanical changes; delete its English, Chinese, and sidecar files together and repair or remove inbound links. A local bug fix, performance change, new capability, or substantive behavior decision does not qualify merely because its implementation is small. Archive other implemented notes only when the shipped decision is complete, its rationale is unlikely to guide future work, and the record still has historical decision value. Keep it active when its alternatives, ownership boundary, negative guarantee, durable or wire semantics, security rule, or reintroduction condition remains useful. Never archive a proposed note: reject an obsolete proposal. Keep a rejected note only while it prevents a plausible mistake; otherwise delete its English, Chinese, and sidecar files together. Use the calibrated [`dsh-archive-agent-notes`](../skills/dsh-archive-agent-notes/SKILL.md) workflow rather than word count, age, or a target quota.
The archive is path-encoded as `archived/{class}/yyyy-mm-dd-topic-title.md`; `implemented` is deliberately absent because only implemented notes can enter it. An archival change moves the complete English/Chinese/sidecar triplet, retains `Status: implemented`, inserts the same `Archived: YYYY-MM-DD` line immediately below that status in both language files, re-records the sidecar, and repairs or deletes inbound links. These are the only permitted content changes during archival.
The archive is path-encoded as `archived/{class}/yyyy-mm-dd-topic-title.md`; `implemented` is deliberately absent because only implemented notes can enter it. An archival change moves the complete English/Chinese/sidecar triplet, retains `Status: implemented`, inserts the same `Archived: YYYY-MM-DD` line immediately below that status in both language files, re-records the sidecar, and repairs or deletes inbound links. These are the only permitted content changes during archival. Existing title punctuation, blank-line layout, and language-switcher wording do not block archival; preserve them with the body.
Once sealed, every archived triplet is permanently frozen. Do not edit, translate, reformat, update, move, or delete it, and do not treat it as authority for current behavior. Documentation gates skip archived sources, including their outbound links; active prose may still link into an archived note when it intentionally cites history. [`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) enforces the closed class tree, complete triplets, archive metadata, sidecar hashes, and the append-only frozen-content manifest. The [archive-policy Agent Note](implemented/process/2026-07-26-frozen-agent-note-archive.md) owns the rationale.
Once sealed, every archived triplet is permanently frozen. Do not edit, translate, reformat, update, move, or delete it, and do not treat it as authority for current behavior. Documentation gates skip archived sources, including their outbound links; active prose may still link into an archived note when it intentionally cites history. [`verify-archived-agent-notes`](../../scripts/verify-archived-agent-notes.ts) enforces the closed class tree, complete triplets, archive metadata, sidecar hashes, and the append-only frozen-content manifest, except for the exact seal transitions documented in [archive instructions](archived/AGENTS.md). The [archive-policy Agent Note](implemented/process/2026-07-26-frozen-agent-note-archive.md) owns the rationale.
## When to write one
Every non-trivial change MUST add or update at least one Agent Note in the same PR. A change is non-trivial when it alters behavior, architecture, a contract shared across files or packages, process or tooling, testing strategy, an on-disk, wire, or configuration format, or another decision a maintainer may reasonably revisit. A proposal for substantial future work starts in `proposed/`; a decision already made starts in `implemented/`. Pick the class folder that matches the decision (see [Classification](#classification)).
Add or update an Agent Note in the same PR only for lasting decision rationale that code, tests, and existing documentation do not explain. A proposal for substantial future work starts in `proposed/`; a decision already made starts in `implemented/`. Pick the class folder that matches the decision (see [Classification](#classification)).
Updating the Agent Note that already owns the decision satisfies the rule; do not create a duplicate. Only a purely mechanical or local edit with no change to behavior, contracts, structure, process, or rationale is exempt. An Agent Note is never edited into a *different decision*: supersede it with a new one, and keep both notes cross-linked unless the old note is later fully consolidated under the rule below. Editing an `implemented/` Agent Note to track where its existing decision lives is required, not forbidden; see [implemented/AGENTS.md](implemented/AGENTS.md).
Updating the Agent Note that already owns the decision satisfies the rule; do not create a duplicate. Mechanical or local edits, including local UI presentation and interaction changes, are exempt. An Agent Note is never edited into a *different decision*: supersede it with a new one, and keep both notes cross-linked unless the old note is later fully consolidated under the rule below. Editing an `implemented/` Agent Note to track where its existing decision lives is required, not forbidden; see [implemented/AGENTS.md](implemented/AGENTS.md).
An implemented Agent Note that is fully superseded may be consolidated into the current owning note and deleted. Before deletion, the owner must preserve every unique rationale, alternative, consequence, required verification, and named coverage gap; repair every inbound link; and delete the Chinese counterpart and consistency record in the same change. Partial supersession does not qualify: keep both notes cross-linked and update every fact that remains current. Consolidation must not rewrite the old file into its opposite or rely on git history as the only copy of rationale.
@@ -5,3 +5,5 @@ Archived Agent Note triplets under the kind directories are frozen historical sn
The archival change may only relocate a complete English/Chinese/sidecar triplet, insert the identical `Archived: YYYY-MM-DD` line below both `Status: implemented` lines, re-record the sidecar, and repair or delete inbound links. Do not inspect, verify, or repair links out of archived notes.
Run the [`dsh-archive-agent-notes`](../../skills/dsh-archive-agent-notes/SKILL.md) workflow and append new artifact hashes with `pnpm run verify-archived-agent-notes --write`. The normal verifier rejects changed or missing sealed artifacts, incomplete triplets, unknown kind folders, and invalid archive metadata.
The authorized Figma-link removal from `feature/2026-08-10-durable-workflow-runs-in-chat` has three exact old-to-new seal exceptions in `scripts/archived-agent-notes.ts` for its English, Chinese, and pairing files. These exceptions permit no other content change, reversal, or deletion.
# Agent Note: A session's agent is composed from a preset cordis.yml
Status: implemented
Archived: 2026-09-18
English | [中文](2026-08-03-per-session-agent-presets.zh.md)
@@ -27,13 +28,13 @@ The presets the deployment ships are the directories under `packages/preset/agen
Mounting is per-session by default. Measured cost for a twelve-row composition is ~3ms and ~600KB per session, so isolation is the cheaper default than any sharing scheme, and a preset authored by a user or by an agent then has the smallest possible blast radius. A preset that genuinely owns an expensive singleton opts into sharing with Cordis's own `isolate` vocabulary: a named realm label is process-global, so two subtrees naming the same label resolve one instance.
Which preset an unnamed session gets is a user setting (`agent-presets.default`) layered over the composition's own`default`, which becomes the `base`. Both layers are needed: the composition value is what a deployment ships and must keep working with no settings provider at all, and the setting is what a person changes without editing a `cordis.yml` they may not own.
The `agent-presets` user-settings namespace carries `modeSelectionEnabled` and `default`. `modeSelectionEnabled` defaults to `true`: the existing new-session picker remains present and an unnamed session resolves to the saved user `default`, or the composition's deployment`default` when none exists. The Web Settings toggle changes only that policy: disabling selection temporarily uses the deployment default, while re-enabling it restores the saved user `default`. This is a deliberate exception to the ordinary user-over-composition settings precedence established in #1539: hiding the chooser disables the user's mode-selection policy without deleting its saved value. The Host policy governs every later session whose caller omits a preset; explicitly named presets and existing sessions remain unchanged. The composition value also keeps the package working with no settings provider, while an enabled user override changes later sessions without editing a deployment-owned `cordis.yml`.
## Consequences
**The effective default is read per resolution, never snapshotted.** A cached value would need a `watch` subscription and a reload path to stay honest, and the resolved scope already re-reads a hot-reloaded document. Reading through is also what makes the boundary correct rather than merely cheap: the new value applies to the next session created, and every running session keeps the composition it was built from. That invariant is the same one the session log enforces from the other side — the header records the id a session was CREATED with and an `agent-preset/selected` event records any later blank-session switch, so a reader resolves the pair (`resolveSessionPreset`) and never the header alone: a resume rebuilds the composition its history was produced under rather than the deployment default at resume time, a cold transcript's presenters resolve in that composition's layer, and the gateway rejects an attempt to adopt a live session under a preset other than the one it currently runs. A snapshot would make the two disagree at exactly the moment the setting changes.
**The effective default is read per resolution, never snapshotted.** A cached value would need a `watch` subscription and a reload path to stay honest, and the resolved scope already re-reads a hot-reloaded document. The Host setting itself applies when an unnamed session is resolved afterwards. An explicit Web Settings action additionally routes its accepted effective default through the existing blank-session selection path only when the captured session id is still current and blank; it never recomposes a running session or rewrites that session's history. The session log enforces the same invariant from the other side — the header records the id a session was CREATED with and an `agent-preset/selected` event records any later blank-session switch, so a reader resolves the pair (`resolveSessionPreset`) and never the header alone: a resume rebuilds the composition its history was produced under rather than the deployment default at resume time, a cold transcript's presenters resolve in that composition's layer, and the gateway rejects an attempt to adopt a live session under a preset other than the one it currently runs. A snapshot would make the two disagree at exactly the moment the setting changes.
**A directly-plugged subtree is invisible to the boot audit.** It never links itself to an `Entry`, so it is absent from `ctx.loader.entries()` and `assertEntriesActivated` cannot see it. The mount audits its own rows instead, reading the tree through an `Include` subclass that publishes it.
**A directly-plugged subtree is invisible to the boot audit.** It never links itself to an `Entry`, so it is absent from `ctx.loader.entries()` and `auditStartupEntries` cannot see it. The mount reads its own rows through an `Include` subclass that publishes the tree and requires every enabled row to activate, independently of the [application startup policy](2026-09-09-consumer-owned-startup-strictness.md).
**A preset can only name a group because the app registers one.** Sharing a realm across rows is a `cordis:group` row, and a preset living outside this workspace — the authored ones under the Harness home, which is the point — cannot resolve `@cordisjs/plugin-group` by name: Node's upward `node_modules` walk never reaches the harness from there. `boot()` therefore registers `cordis:group` beside `cordis:include` as a loader builtin, so both load through the ambient module pipeline rather than through the included tree's own specifier resolution. Without it the `isolate` vocabulary above is expressible one row at a time only, and a provider could never be grouped with its consumers.
@@ -61,11 +62,11 @@ Which preset an unnamed session gets is a user setting (`agent-presets.default`)
**A preset's package names must resolve from the harness, not from the preset.** `EntryTree.import()` resolves a row against its own tree's `baseUrl`, which `Include` sets to the composition's directory. That is right for a relative specifier and fatal for a package name: a locally authored preset lives under the user's home, where Node's upward `node_modules` walk never reaches the installed harness, so every `@deepseek-ai/dsh-*` row fails to import and the whole preset is unmountable. The shipped presets hid this — they sit inside the install. The mount records the host composition's base before plugging the subtree and sends bare specifiers there, leaving relative paths resolving from the preset so its own files still travel with it. The real-composition test writing a preset into a temp root is what found it.
**The preset id is model-visible and must be logged.** It determines the tool set and prompt, so a resumed session has to restore the same composition; recording it is a session fact, not runtime state. It rides the session header beside `cwd`, and the summary carries it so a picker shows what a session actually runs rather than the deployment's current default.
**The preset id is model-visible and must be logged.** It determines the tool set and prompt, so a resumed session has to restore the same composition; recording it is a session fact, not runtime state. It rides the session header beside `cwd`, and the summary carries it so the client surfaces show what a session actually runs rather than the deployment's current default.
**A durable header field is not durable until the provider writes it.** `agentPreset` landed on `SessionHeader` with the right rationale and the JSONL provider omitted it; the derived query index also maps header fields explicitly, so a resumed Session came back with no preset and the surfaces that name it fell silent. `summarizeCold` had the same form — it hand-built the cold list row instead of reusing the shared projection. A field declared durable needs a test that crosses a real store, not only the type that declares it.
**The choice belongs to the screen where it still works.** The composer seat spent almost its whole life disabled, since the preset is fixed once a turn has run. It moved to the new-session screen beside the workspace picker, where the pick is *staged*: that screen precedes the session it applies to, and the stage lands when a session becomes current and is still blank — covering both the session a workspace connect creates and the blank one it reuses, which riding `sessions.create` would miss. It is spent on first use, matching the workspace picker beside it. What a running session runs is then a read-only label in its header: a control there would promise a switch the host refuses outright.
**The choice belongs to the screen where it still works.** The control lives on the new-session screen beside the workspace picker, is present under the default-on Host policy, and disappears only after `modeSelectionEnabled` is disabled. Its pick is *staged*: that screen precedes the session it applies to, and the stage lands when a session becomes current and is still blank — covering both the session a workspace connect creates and the blank one it reuses, which riding `sessions.create` would miss. It is spent on first use, matching the workspace picker beside it; hiding the picker discards a stage that has not reached a session and returns the current blank session to the deployment default through the same selection path. What a running or historical session runs remains a read-only label in its header: a control there would promise a switch the host refuses outright.
**A preset multiplies a cost the host was already paying: nothing disposes an agent.** Measured against the shipped compositions with `--expose-gc`, one live agent holds ~0.17 MB on `minimal` and ~1.31 MB on `standard`/`cordis`, mounting in ~38 ms and ~135 ms; the first agent of a process costs ~7 MB more as Node imports the modules, which every later mount then shares. Growth is strictly linear — 10, 30 and 50 agents give the same per-agent delta — and disposal reclaims essentially all of it (50 `standard` agents held 57.8 MB and returned it). So the object graph does not leak; the lifecycle does. `ApiSessionAgentController` discards the `AgentHandle` returned by the registry, `archiveSession` only edits the workspace registry, `AgentRegistry` has no eviction, and the sole disposal site in the host is the JSON-RPC server's own shutdown. A web host therefore retains every session it has touched, at ~1.3 MB each once presets are composed rather than ~0.2 MB before. Note that pruning the mount registry does not help here: it drops records whose fiber `uid` has cleared, and an agent that never dies never clears one.
# Agent Note: Durable image offload by surface replacement
Status: implemented
Archived: 2026-09-10
English | [中文](2026-09-02-durable-image-offload.zh.md)
## Problem
Request-size image offload was recomputed from scratch on every request. Each route collected every image occurrence on the derived surface, oldest first, and once the accumulated bytes exceeded its budget it rounded the excess up to a whole removal quantum and replaced that many oldest occurrences with placeholder text, per the [unified image request pipeline](../feature/2026-08-20-unified-image-request-pipeline.md). Nothing remembered where the previous request stopped; the prefix was stable only because the arithmetic over an append-only history repeated itself.
That stability failed wherever the arithmetic inputs moved. The [Files inline fallback](../../archived/bug-fix/2026-08-21-deepseek-files-inline-fallback.md) rebuilt a request under a 20 MiB inline budget with a 10 MiB quantum, offloading far more images for that one request, and the next request in file mode brought them back. The pi-ai route used a quantum of one byte, so its prefix moved on almost every request. Compaction lowered the total and returned previously offloaded images. A route switch moved every step boundary. Each move changed the model-visible prefix and invalidated the provider cache prefix.
The same recomputation broke the repository invariant that model-visible input is reconstructable from the session log. Which representation was dispatched, the exact derived request-version byte lengths, and the route budgets and quanta were runtime or configuration facts that never entered the log; `request/header` records call config, system prompt, and tools only. Provider usage anchors token totals and cannot recover the image set, and the [route-priced estimate](../../archived/feature/2026-08-24-route-priced-image-request-pressure.md) documented that it did not reproduce the fallback budget. No consumer could pair a logged assistant response with the image set its request carried.
## Decision
The offload of an image occurrence is a durable surface fact recorded the way compaction records its reductions: a `surfaceOp: replace` node.
**Marked copies on the surface.**`ImageBlock` gains `offloaded?: true`. An offloaded occurrence lives in a replacement event of the same type as the node that carried it (`user/message` or `tool/result`) whose content is a copy of the original with those blocks marked; `sourceEventSeqs` points at the replaced node. The session core, its event map, its derivation, and its validation are unchanged. `deriveMessages()` sends the marked block; serialization renders every marked block as `offloadedImageText` with the currently resolved access path and prepares only retained occurrences.
**Only advances.** A replacement never reverts. When a budget grows, a route changes, or compaction lowers the total, the marked copies stay, so the model-visible prefix and the provider cache prefix move only forward.
**Adapters project, never decide.** An image-capable route enforces an `LlmImageRequestBudget` (`representation`, `maxBytes`, `maxImages`, both quanta) over the retained occurrences' exact request-version bytes. When they still exceed the budget, in file mode, under the inline fallback's tighter budget, or under the pi-ai bound, the adapter fails the attempt with `IMAGE_OFFLOAD_REQUIRED` and `LlmFailure.offloadImages` naming how many more oldest occurrences must be offloaded, computed with the shared `requiredImageOffload()`. Nothing plans an offload before dispatch.
**Recovery owned by `dsh-compaction-image-offload`.** Image offload is compaction in another capacity dimension: the provider rejects a request, durable history is reduced, the step retries. The executor is a sibling of `compaction-tool-result-pruner` in the compaction group and listens on the `agent/request-error` waterfall. On `IMAGE_OFFLOAD_REQUIRED` it walks the surface in model request order, marks the first `offloadImages` retained occurrences, and for each node that carried one appends the seam's `compaction/prune` shadow price followed by the marked copy, then returns the `retry` action without spending the provider retry budget or logging `llm/retry`. Assistant nodes carry model output, not input images, and are skipped. When nothing remains to offload it delegates and the failure reaches ordinary recovery. The loop re-runs the step over the replaced surface and logs the fresh `request/header` every surface replacement gets; the agent loop is unchanged.
**Token accounting.**`priceImages` receives the surface's `ImageBlock`s and prices a marked one as its placeholder text; the DeepSeek and replay pricing no longer reproduce any offload arithmetic. The meter needs no new state: the `compaction/prune` event and the replacement reprice the node like a tool-result prune.
**Other consumers.** Compaction reconstructs each selected event through `deriveEventMessage()` and sees the marks. Resume, fork, and replay reproduce the surface from the log. Text-only routes keep their separate whole-history substitution.
## Alternatives considered
**Keep recomputing the offload point per request.** Stable only while the arithmetic inputs held still; the inline fallback, the pi-ai quantum, compaction, and route switches all moved the prefix, and no consumer could reconstruct a historical request's image set.
**Record each request's projection outcome as a log-only event.** Restores reconstructability but not stability: the recorded outcome is not a decision input, so every oscillation still happens and the log merely documents it, with two sources of truth for the offloaded set that can disagree.
**Log the full projected request body.** Everything except the offload decision is already derivable; repeating the history per request grows the log quadratically to record one position.
**A log-only `image/offload` watermark event applied by session derivation.** An earlier revision recorded the offload point as a position (event seq plus block path) and had `Session.deriveMessages()` mark every occurrence at or before it. That gave the session a second mechanism for changing model-visible history next to `surfaceOp: replace`, with its own validation, fold, cache invalidation, and required-on-read event, and placed the logic in the core instead of on the plugin that decides. Issue #3041 asks that a permanent eviction use a surface-changing event rather than a request-projection event; because the offload never reverts, it is one.
**Let each adapter append the replacement.** The adapter owns the budgets but not the session surface; a surface change appended below the loop lets two adapters define the surface differently. Adapters report the count they need instead.
**Plan the offload before dispatch, in the loop or in a plugin.** The loop knows the exact prepared route before deriving each request, so planning there never spends a failed attempt; but it puts a route-specific policy into the one component every profile shares, changes the documented step order, and bypasses the `agent/request-error` waterfall that context-overflow compaction and retry already use for the same repair-and-retry pattern. A pre-step plugin keeps the loop unchanged but cannot see the step's own messages or the first request's route, so the failure path stays necessary, and the plan needs every route to declare its budget on its model info. Handling only the failure costs one attempt per quantum crossing (64 MiB in DeepSeek file mode, 20 MiB on pi-ai) and matches the planned direction: routes will stop checking sizes locally, send every image, and the provider will report what it cannot cache, which is exactly a failure naming an offload point.
**Keep a transient extra offload for the inline fallback and exact-byte overflow.** Would have sent an unlogged projection in exactly the cases the invariant exists for; the failure-and-advance path costs one serialization attempt and keeps every dispatched request derivable from the log.
## Consequences
Offloaded images never return automatically when budgets grow, a larger route is selected, or compaction lowers the total; recovery is the read-only path in the placeholder, which the model uses deliberately. A Files outage or a temporary switch to a small-budget route offloads permanently; both are accepted for the same reason. Each offload copies the carrying node's message into the log; images are references, so the copy is the node's text plus block metadata.
Every dispatched request's image set is determined by the log alone, across file mode, inline fallback, resume, fork, retry, and compaction, and the provider cache prefix no longer oscillates. The execution-world access path embedded in placeholder and handle text is still resolved at serialization time; that gap exists for retained images too and belongs to a separate decision about recording the execution-world mapping. The route-local byte checks are provisional: once the provider reports uncacheable images itself, `requiredImageOffload()` and the route budgets go away while the marked copies and the recovery branch stay as they are.
## Testing
`packages/llm/llm/tests/content.spec.ts` pins arbitrary-depth image traversal, the offload count including the 129-to-64 MiB quantum example, and placeholder projection. `packages/compaction/compaction-image-offload/tests/image-offload.spec.ts` pins the `IMAGE_OFFLOAD_REQUIRED` replace-and-retry path with its `compaction/prune` shadow prices and without a retry event, the untouched original node, request-order counts after an earlier surface replacement, and the exhausted case that delegates downstream. Adapter specs pin placeholder projection, prepared-only-retained reads, and the exact-byte failure with its count; `route-pricing.spec.ts` pins placeholder pricing for a marked replacement. The `inline-image-prompt` TypeScript SDK snapshot replays an authored `IMAGE_OFFLOAD_REQUIRED` attempt through the shipped profile and pins the replacement and the retried request.
English | [中文](2026-09-09-desktop-in-place-profile.zh.md)
## Problem
Staging preserves an old plugin installation but adds profile copying, directory moves, a recovery journal, and rollback state. Local plugin changes accept explicit repair after failure instead of this complexity.
## Decision
Application-owned package retention is qualified by the [production cleanup decision](../bug-fix/2026-09-15-desktop-profile-core-cleanup.md).
Desktop stops the Host and modifies the current profile directly. Shared app-boot cleanup detaches its own fallback links before package changes; the Host’s shared profile runner supplies required links on startup. Package locking and configured lifecycle scripts remain. Upgrades refresh module links without copying plugin files.
Package or Host failures retain partial changes for repair and retry. There is no staging profile, activation journal, directory-swap recovery, or automatic rollback. Existing scratch directories are not interpreted or deleted.
This supersedes staging and rollback in [the packaging decision](2026-08-25-electron-desktop-packaging-and-updates.md), [the bundled-runtime decision](2026-09-08-desktop-bundled-runtime-and-external-plugins.md), and [the immediate-window decision](2026-09-09-desktop-immediate-window-and-direct-start.md). Host boot follows the [thin-wrapper decision](2026-09-10-desktop-web-wrapper.md); release, module ownership, and window lifecycle remain separate decisions.
Desktop delegates installation and lifecycle scripts to pnpm, without a pending-operation startup gate, frozen-lockfile reinstall, or automatic rebuild. Failed package operations preserve partial changes and leave disable, remove, and startup retry available. The Host inherits the user environment, and profiles may use directory links. An unchanged legacy Desktop-generated pnpm configuration is replaced with the Web defaults; customized configuration remains user-owned.
## Alternatives considered
Staging protects the previous installation at the cost of copying and crash recovery. Versioned directories still need preparation, selection, and cleanup. Direct writes give up automatic recovery; reintroduction requires an unattended-recovery product requirement that justifies these costs.
## Consequences
Tests cover offline initialization, in-place upgrades, failure before writes, retained changes after Host failure, partial pnpm failure, restored host links, and exclusive package ownership. Signed application and GUI acceptance remain release-environment checks.
# Agent Note: Local Office preview through WASM PDF bytes
Status: implemented
Archived: 2026-09-11
English | [中文](2026-09-10-local-office-preview.zh.md)
## Problem
Office Open XML files are ZIP archives, so text fallback cannot provide a useful preview. Document conversion must stay local without taking focus or adding document content to a model conversation. Engine success alone cannot prove that the input has the claimed format or that a PDF was produced.
## Decision
A [document-render Service Definition](../../../../packages/document/document-render/README.md), a LibreOffice WASM provider, and Client/API consumers form the conversion capability. [Boot composition](../../../../packages/document/document-render-auto/README.md) mounts the provider and Remote controller only when `wasm.artifactDirectory` is configured. Invalid explicit configuration fails initialization. The Office Client registration always exists so an unconfigured Host receives guidance. Availability follows the generated Remote namespace without executable discovery or a duplicate Client flag.
The [WASM provider](../../../../packages/document/document-render-libreoffice-wasm/README.md) uses pinned official LibreOffice source, Emscripten, and a LibreOfficeKit adapter. Each Node Worker owns one engine instance, its pthreads, and its memory filesystem. Terminating the Worker cancels synchronous engine calls; every conversion waits for its Worker to stop before returning. OOXML inspection verifies archive membership, content type, and resource bounds before startup.
A VCL callback requests a font family before substitution or missing Unicode characters during glyph fallback. Host code indexes installed font metadata and copies selected files into engine memory, including complete font collections. Exact installed families precede configured alternatives; original-first fontconfig aliases preserve that ordering inside LibreOffice. Optional initial families use the same resolver. The engine has no Host filesystem mount, and rendering neither installs nor downloads fonts. The [build recipe](../../../../native/libreoffice-wasm/README.md) records source and toolchain revisions, patches, and asset hashes. Engine bundles remain immutable during provider use; a different build uses a new directory.
The [independent engine release](../process/2026-09-11-independent-libreoffice-package.md) owns precompiled npm packaging and keeps engine compilation outside ordinary DSH builds.
The provider returns caller-owned PDF bytes and independent `succeeded`, `timedOut`, and `cancelled` facts. PDF bytes are copied out of engine memory before teardown. The API consumer authorizes the source through [Workspace Files](2026-09-09-workspace-file-read-authority.md), requires successful uninterrupted conversion, and encodes the PDF directly. Source path and freshness version survive the PDF transport. Workspace Files limits source reads; the provider's `maxOutputBytes` alone limits generated PDFs. No temporary PDF or file lease is needed.
The Office Client watches the selected Conversation's existing Deliverables projection and warms recent Office files without opening tabs. Its bounded memory cache checks authorized source metadata on every read and shares pending conversions between background and foreground callers. Only the last departing reader cancels a shared conversion; failures are not cached and connection reset discards cached bytes. [Document Preview](2026-09-08-document-preview-operations.md) owns format selection, loading, cancellation, and the PDF.js Worker.
PDF.js's official TextLayerBuilder owns selection boundaries and copy normalization over the width-fitted canvas, with shared page cleanup and a component-owned resize observer. Its end-of-content marker and stacking rules constrain selection in blank regions; line-break highlighting is suppressed. Per-page cancellation uses the builder's cleanup rather than aborting the first page's signal, because the official selection listeners are shared across pages.
Missing-family observations accompany the PDF through the worker and Remote response. The Office plugin renders a keyed notice above the shared PDF scrollport, so dismissing it removes its layout height. The font resolver intersects explicit source and theme references with actual document requests, excluding engine defaults and font-table inventories; installed aliases and glyph fallback do not by themselves mean a family is absent.
## Alternatives considered
**Keep an installed native LibreOffice provider.** A second provider adds executable discovery, version queries, subprocess supervision, scratch files, and platform-specific confinement. This design uses WASM for all supported Office previews. A native provider needs a demonstrated rendering or deployment requirement that WASM cannot satisfy.
**Return a temporary PDF path.** The Worker already supplies independent PDF bytes. Writing them to disk, authorizing another file read, and maintaining leases adds file ownership without a current consumer. Source authorization and output limits apply directly to the memory result.
**Automate installed Microsoft Office.** Word for Mac requires GUI automation rather than headless conversion, introducing focus changes and per-file operating-system authorization. Native Office automation remains outside this preview capability.
**Use an online converter or download the engine automatically.** Remote conversion uploads document content. Automatic installation adds runtime distribution and update responsibilities; explicit artifact configuration leaves deployment with the operator.
**Convert PDF pages to PNG.** PDF.js already owns PDF presentation. Rasterizing adds another image pipeline and loses the existing PDF controls and selectable text.
**Add a model-facing rendering tool or durable preview events.** Client preview does not supply model input. A model-facing tool requires logged facts and both SDK projections, so it is a separate consumer.
**Scan every font-table entry for warnings.** Font tables include unused styles and templates. Reporting them would make the notice unrelated to rendered content; collection instead starts after engine initialization and ends after PDF save.
## Consequences
The single-provider design assumes WASM covers the required Office rendering behavior; it is not evidence of complete fidelity equivalence with native LibreOffice. Fidelity depends on the engine build and installed fonts. The [real-engine tests](../../../../packages/document/document-render-libreoffice-wasm/tests/libreoffice-wasm.e2e.ts) cover DOCX, XLSX, PPTX, selectable Chinese text, and embedded system fonts; ABI fixtures alone cannot establish rendering fidelity or assembled browser presentation.
WASM instances have substantial memory and startup costs, so the provider bounds concurrency. The [font reuse and image resolution decision](2026-09-11-wasm-preview-font-and-image-budgets.md) owns shared Host metadata, Worker request memoization, and raster export limits; only selected fonts enter engine memory. Large collections require bounded memory growth, and CFF export requires checked stack space. A fatal abort prevents further calls into C++. A device without a font covering a character cannot render it correctly without additional fonts. Owning the WASM build adds compiler compatibility, patch maintenance, and redistribution obligations.
Prewarming consumes conversion resources to reduce foreground waits; entry and byte limits bound retained Client results. Source and PDF bytes remain outside Session storage and persistent caches. The [provider tests](../../../../packages/document/document-render-libreoffice-wasm/tests/provider.spec.ts) cover independent output facts, input/output rejection, queued cancellation, and active Worker termination. Existing preview registry, file read authority, and Session ownership decisions remain active.
# Agent Note: Font reuse and image resolution in WASM previews
Status: implemented
Archived: 2026-09-11
English | [中文](2026-09-11-wasm-preview-font-and-image-budgets.zh.md)
## Problem
Office conversion repeats font metadata parsing across documents and font matching within one document. Image-heavy documents also spend substantial PDF export time resampling images above preview resolution. These costs need separate controls because font reuse does not reduce image decoding or resampling.
## Decision
The [WASM provider](../../../../packages/document/document-render-libreoffice-wasm/README.md) builds a font metadata snapshot once during initialization. Each conversion Worker receives a structured clone. The Host retains names, face attributes, paths, sizes, and modification times; glyph coverage, request caches, and imported bytes belong to the Worker. Reloading the provider refreshes the snapshot. Workers validate indexed files when reading them, and an already imported font remains usable within that conversion.
Each Worker memoizes complete requests: family, style, weight, italic, width, pitch, language, and ordered code points. The cache stores MEMFS paths and missing-family observations. Hits replay those observations because an initialization request can recur after document font collection starts. Worker termination releases both the cache and engine memory.
Runtime PDF filter options reduce raster images to a configurable `maxImageResolution`, defaulting to 192 DPI. The [shared PDF canvas](../../../../packages/client/ui-sidebar-documentpreview/src/client/pdf/document.ts) renders at 96 CSS DPI times the device pixel ratio; the default covers ratio 2. Text and vector graphics remain scalable. Explicit bookmark export preserves LibreOfficeKit's default when JSON filter options replace its implicit filter data.
The [local Office preview decision](2026-09-10-local-office-preview.md) continues to own conversion lifetime, authorization, missing-font presentation, and PDF transport.
## Alternatives considered
**Index fonts inside every conversion Worker.** This keeps discovery off the Host event loop and sees newly installed fonts immediately, but repeats full-file metadata parsing across previews. A provider snapshot removes that repeated work at the cost of synchronous initialization and explicit reload after font changes.
**Cache only the family name.** Style, language, pitch, and missing characters can select different files. A complete request key retains those distinctions and still captures repeated layout requests.
**Keep 300 DPI for every preview.** Higher raster resolution retains detail for high pixel ratios and magnification but increases image export work. The configurable 192-DPI default follows the common display target without rasterizing text or rebuilding the engine.
## Consequences
Cold provider startup includes indexing and remains separate from conversion deadlines. Shared metadata retains no original font buffers, while each Worker still reads selected fonts and lazily decodes coverage. Installing or replacing fonts requires provider reload; a file changed since indexing can fail a later conversion.
Reducing image resolution trades raster detail at high magnification for less export work. It is not an engine memory cap: LibreOffice can still decode full-resolution source images, load large font collections, or exhaust its build-time WASM memory maximum. No persistent font cache, filesystem watcher, or cross-document engine instance is introduced.
[Font tests](../../../../packages/document/document-render-libreoffice-wasm/tests/fonts.spec.ts) cover complete request keys, repeated missing-family observations, snapshot isolation, and failed imports. [Provider tests](../../../../packages/document/document-render-libreoffice-wasm/tests/provider.spec.ts) cover snapshot reuse and reload. The [real-engine tests](../../../../packages/document/document-render-libreoffice-wasm/tests/libreoffice-wasm.e2e.ts) inspect exported image dimensions with default and overridden DPI settings alongside selectable text and page count; ABI forwarding alone cannot establish filter behavior.
[字体测试](../../../../packages/document/document-render-libreoffice-wasm/tests/fonts.spec.ts)覆盖完整请求键、重复缺失字体族观察值、快照隔离和失败导入。[提供方测试](../../../../packages/document/document-render-libreoffice-wasm/tests/provider.spec.ts)覆盖快照复用和重新加载。[真实引擎测试](../../../../packages/document/document-render-libreoffice-wasm/tests/libreoffice-wasm.e2e.ts)检查默认及覆盖 DPI 设置时的导出图像尺寸、可选择文本和页数;仅验证 ABI 参数转发无法证明过滤器行为。
# Agent Note: Schedule in the shipped Web composition
Status: implemented
Archived: 2026-09-25
English | [中文](2026-09-24-web-default-schedule-composition.zh.md)
## Problem
The Automation tasks page, the Session reminder catalog, and the `schedule_*` tools reached a Web deployment only through `--patch apps/cli/config/examples/schedule/cordis.yml`. `packages/bundle/web-app/cordis.patch.yml` carried the `ui-schedule` client row with `disabled: true`, and the Host rows `time-context` and `schedule` existed in that overlay alone. A person running the shipped `web` profile therefore saw no Automation tasks entry and no reminder tools, while every consumer that wanted them repeated the same three rows: the repository preview image kept its own overlay list, and two Web suites hard-coded the overlay path.
## Decision
`packages/bundle/web-app/cordis.patch.yml` inserts `time-context` and `schedule` in its Host row list and leaves `ui-schedule` enabled; `packages/bundle/web-app/package.json` declares both packages, which `verify-cordis-config` requires for a bare row name in a bundle patch. The `web` profile consequently ships the Automation tasks page, the Session-header reminder clock, the idle Session row's clock mark and hover list, the right-Sidebar task tab, and `schedule_create`, `schedule_list`, `schedule_update`, and `schedule_delete` on every live root Agent.
`apps/cli/config/examples/schedule/cordis.yml` is deleted. `applyEntryPatches` appends an `insert` list without de-duplicating ids, so keeping the overlay would mount `time-context` and `schedule` a second time; the two Web suites and the preview packer that named the overlay compose the shipped profile alone.
`time-context` ships with Schedule because a reminder request states a wall-clock target. The plugin appends one durable user message per eligible step carrying the sampled instant, the browser zone attached to the open request, and the elapsed time since the preceding model-visible message. The sampling instant is what lets the model turn a request such as "tomorrow at nine" into an offset-bearing `at` value; the [Schedule subsystem](../../../../docs/subsystems/schedule.md) owns that interpretation boundary and the explicit-zone requirement it feeds.
The Host service, its storage domain, and the client half are unchanged. This decision moves which composition mounts them, and the [Host-owned scheduled messages decision](2026-09-16-host-schedule-storage.md) continues to own task storage, activation, dispatch, and delivery records.
## Recorded sessions
A Web scenario that drives a live step now logs one time-context reading per step, so its committed Session fixture and header sidecars were refreshed. A reading's sampled instant, browser zone, and elapsed duration are volatile, and `normalizeWebSessionVolatiles` in `apps/web/tests/scaffold.ts` replaces them with `{{timeContextTimestamp}}`, `{{clientTimeZone}}`, and `{{elapsed}}`. The turn and step numbers and the preceding-event baseline stay readable, so a fixture still shows what the model received. Refresh writes the current writer generation (`session.v4.jsonl`) beside the retained predecessors, which remain committed replay baselines.
## Alternatives considered
**Keep the overlay and change nothing.** The shipped surface would keep contradicting the product documentation that describes the Automation tasks entry, and each consumer would keep its own copy of the three rows. The overlay also cannot satisfy the request that the default `dsh web` process exposes the page without a flag.
**Ship the Schedule rows without `time-context`.** The model would then have no sampling instant of its own, so an unqualified date or time in a reminder request would depend on a separate clock source; the overlay paired the two rows for that reason.
**Reach for a separate optional bundle.**`OPTIONAL_BUNDLES` exists for bundles a person switches on from the plugin manager, which is the same opt-in shape as the overlay; the decision here is that a default Web surface owns the page and the tools.
## Consequences
- Every Web session's request header carries four additional tool schemas, and every eligible step appends one durable user message. A conversation that never creates a reminder pays that token cost.
- The clock reading is model-visible and durable, so it replays, compacts, and appears in exported Session logs like any other user message.
- A deployment that wants the previous behavior disables the `time-context`, `schedule`, and `ui-schedule` rows in its own profile patch layer; the capability itself is untouched.
- The `cordis_inspect_query``listTools` answer for a full preset table now passes the base composition's 12,500-token inline budget, so the spill policy retains that answer's head and tail with a spill path instead of the complete JSON.
- The repository preview image and both Web suites drop their overlay arguments, so one composition change reaches every surface at once.
English | [中文](2026-09-09-content-sized-diagram-previews.zh.md)
## Problem
Short Graphviz and SVG images leave large empty regions when displayed inside 400px frames. A 59px Graphviz diagram occupies a 400px preview even though its rendered dimensions are already available to the browser.
## Decision
Mermaid, Graphviz, and SVG share the inert image canvas in `SourcePreview`. The browser derives its height from the image, preserves aspect ratio when shrinking to the available width, and adds 16px padding. Source switching retains the image and copying reads the original code. Theme changes regenerate Graphviz colors without introducing a separate sizing observer.
This replaces the diagram-frame choice in [Static Markdown fence previews](../feature/2026-09-09-markdown-static-previews.md); that note continues to own HTML isolation, rendering lifecycle, and license obligations. HTML retains its opaque, script-free, 400px frame. SVG image mode disables scripts, link interaction, and external resource loading without inserting source-controlled markup into the application document.
## Alternatives considered
**Set iframe height to auto.** An iframe does not derive its outer height from its inner document. This still reserves an unrelated viewport.
**Add a frame measurement script or same-origin access.** A diagram already has image dimensions. Extra execution or origin permissions and resize messaging are unnecessary for that content.
## Consequences
Short diagrams occupy only their rendered height and canvas padding. The browser geometry regression compares image and container height in light and dark modes, then verifies proportional scaling in a narrow viewport. Browser security checks include SVG scripts, event handlers, nested HTML, and external images; copy and source toggling remain covered. HTML automatic height remains outside this change because arbitrary document layout has different measurement and isolation requirements.
# Agent Note: Session Controller forks stop at the selected turn end
Status: implemented
Archived: 2026-09-16
English | [中文](2026-09-11-session-controller-fork-turn-cut.zh.md)
## Problem
A user input enters the durable inbox before its `turn/start`. Extending a completed-turn fork through the following between-turn events can copy the next input's insertion without its later removal. Continuing the child then executes an input from beyond the selected turn.
## Decision
The [Session Controller](../../../../packages/api/session-controller/README.md) copies the contiguous prefix through the selected `turn/end`, inclusive. Explicit anchors select the first closing event at or after the anchor; omitted and past-end anchors select the last closing event. No event after that closing event belongs to the seed, including queued input, titles, and model settings.
The lower-level `SessionStore.fork()` retains its explicit stable-event semantics from the [log-only event decision](../simplification/2026-07-28-remove-synthetic-log-only-turns.md). Selecting a completed turn in the controller does not request a later stable event.
## Alternatives considered
**Stop only at the next inbox event.** Event-type exceptions still copy unrelated state changes after the selected turn and require the controller to classify plugin-owned events.
**Copy the tail and clear the child's inbox.** Clearing adds child events to cancel input that lies outside the requested prefix, while still inheriting other state from after the selected turn.
## Consequences
A fork inherits the configuration recorded through its selected turn. Later configuration and title events are excluded. The client may independently assign the child's fork title. Events already inside the selected prefix retain their ordinary replay semantics; this decision does not redefine pending input inserted before the selected closing event.
## Verification
Controller tests execute the production loop and check that sending C after forking A excludes the parent's later B from child history and produces one model request. Message, closing-event, omitted, and past-end anchors share this assertion. Model-routing coverage excludes a later configuration change. The Web message-actions snapshot seeds a queued input after the completed turn and verifies the branch action creates a child without that input or its inbox insertion.
English | [中文](2026-09-16-movable-mandatory-update-window.zh.md)
## Problem
The Windows mandatory-update page used a frameless modal overlay sized to the product window. The modal disabled its parent, so the parent's native title bar could not be used to move or maximize either window. Closing the overlay was intercepted, leaving no visible exit control.
## Decision
On Windows, mandatory policy uses a separate native framed modal with move, resize, and maximize controls. The parent remains disabled while policy blocks interaction. Closing the modal requests normal application shutdown; it never dismisses policy and resumes the product window. Other platforms retain the existing overlay presentation. The [mandatory-update decision](../feature/2026-09-11-desktop-mandatory-update-client.md) still owns policy and installation authorization.
## Alternatives considered
**Keep the full-content overlay and add a drag region.** A drag region would move the disabled parent indirectly and would not restore native maximize or close controls.
**Let close dismiss the policy page.** That would expose the blocked product window without a fresh no-force policy response.
## Consequences
Windows users can place or maximize the update window and exit the application from its close button. Modal blocking and the second installation approval remain intact. Installer-owned quit disposes the modal before Electron closes windows, so its close guard cannot block installation. The native frame replaces the dimmed full-content overlay on Windows; the ordinary update dialog keeps its existing overlay.
Windows 强更页面原先使用与产品窗口等大的无边框模态覆盖层。模态窗口禁用了父窗口,因此无法使用父窗口的原生标题栏移动或最大化窗口。覆盖层的关闭操作又被拦截,界面上没有可用的退出控件。
## Decision
在 Windows 上,强更策略使用独立的原生有边框模态窗口,支持移动、调整大小和最大化。策略阻塞期间父窗口仍不可操作。关闭模态窗口会请求应用正常退出,不会取消策略并恢复产品窗口。其他平台保留现有覆盖层展示。[强更决策](../feature/2026-09-11-desktop-mandatory-update-client.zh.md)仍负责策略与安装授权。
@@ -25,7 +25,7 @@ The renderer gives each level a distinct visual responsibility. The run uses a 3
Navigation is derived from two current authorities rather than persisted. A member row is interactive only while its durable member state is running and the current ordinary Session list contains the same id with `origin: 'subagent'`, `parentId` equal to the displayed parent, and `running: true`. Underlined member text is the only visible affordance; keyboard focus draws a two-pixel business-primary ring around the name area, and the fixed status label remains the lifecycle word rather than an action instruction. The renderer invokes only the injected ordinary `sessions.open(id)` callback. Addressed-only, remote, wrong-parent, and terminal members remain visible but static.
The [seven-state Figma reference](https://www.figma.com/design/tguwzZRmHCjbq58mfsqT0M?node-id=5-2) fixes the information hierarchy for running expanded/collapsed, completed history/expanded, failed plus cancelled, interrupted recovery, and dark narrow presentation. Repository `DisclosureRow`, `StateDot`, icons, semantic tokens, and keyed-node behavior remain the implementation authority; the reference introduces no runtime field or state owner.
The seven-state Figma reference fixes the information hierarchy for running expanded/collapsed, completed history/expanded, failed plus cancelled, interrupted recovery, and dark narrow presentation. Repository `DisclosureRow`, `StateDot`, icons, semantic tokens, and keyed-node behavior remain the implementation authority; the reference introduces no runtime field or state owner.
# Agent Note: One session-header task list — jobs joined with activities in the UI projection only
Status: implemented
Archived: 2026-09-03
English | [中文](2026-08-25-unified-task-list.zh.md)
Superseded: the two-roster join described here was removed with the activity seam — the job rows are now the whole roster; see [jobs absorb the record](../architecture/2026-09-01-jobs-absorb-activity-record.md).
## Problem
The [activity observation seam](2026-08-24-activity-observation-seam.md) shipped its Web surface as a second session-header button beside the [background-job list](2026-08-08-web-background-job-display.md). A background bash run registers in both planes — `ctx.jobs` for control and `ctx.activities` for observation — so the same work rendered twice: once in "background jobs" with lifecycle and duration, once in "activities" with expandable live output. Two near-identical popovers with different names for overlapping row sets read as a product accident, and the flat activity rows (live and settled interleaved, single-line, no duration) had no visual hierarchy.
## Decision
The service planes stay orthogonal; the join happens once, per row, in the UI projection.
- **`ctx.jobs` and `ctx.activities` remain separate services.** Jobs are the control plane: model-visible ids, a consuming `readOutput` cursor the model drains, `stopping`, `kill`, and the `reported` completion-notice contract. Activities are the transient observation plane: non-consuming absolute offsets, invisible to the model, gone on restart. Merging the services would force one registry to carry both a consuming and a non-consuming cursor and would blur the "model-visible ⟺ logged" line for a plane designed to stay off the session log.
- **`dsh-client-ui-activity` renders the single merged list; `dsh-client-ui-jobs` is deleted** (pre-release, no compatibility shim), and the merged entry takes the job list's slot order. Each `jobsBySession` row is joined with the activity carrying its `correlation.jobId`: the job supplies identity, lifecycle (`stopping` exists only there), duration, and the model-visible `detail`; the activity supplies the expandable output panel. An activity without a job (a workflow run) keeps its own row; an activity whose job is not in the projection is not dropped. Without the activity registry, job rows still render — they just offer no panel, so the merged control degrades to exactly the old job list.
- **User-facing vocabulary is "task" (任务)** — `N tasks running` / `N 个任务进行中` — because every row is running work to the user, while "background job" excludes workflow runs and "activity" names the internal plane, not the user concept. The dictionary namespace and package names stay `activity`: internal names follow the naming ledger, not display copy.
- **Hierarchy**: live rows first (label over a kind-badge-and-status second line, ticking duration), then settled rows compacted to one de-emphasized line; each non-empty section carries its own heading (Running / Finished). Rows without an observable activity render as static rows with no expansion affordance.
Deliberately not joined: subagent delegation rows stay bare job rows (the subagent panel is their surface), and foreground commands stay in their tool cards — neither gains an activity producer.
## Alternatives considered
- **Merge the services** — rejected for the cursor-semantics and model-visibility reasons above.
- **Keep the job list and bolt output onto it** — leaves workflow runs (activities without jobs) homeless, forcing a second list to survive anyway.
## Refinements from browser acceptance
Driving the merged list in the real app produced four presentation decisions, all scoped to the expanded panel and the header band:
- **The header band order is preset → tasks → subagent catalog.** The root-session descendant count moved out of the breadcrumb (`header.lineage`) into the actions band (order 30) and dropped its `/` separator — a root session has no hierarchy to breadcrumb; child sessions keep their `root / child` switcher where the `/` is real. `SubagentCatalogAction` owns the moved seat; the lineage renderer now renders nothing on roots.
- **The panel's copy control copies the command** (`TerminalBlock.copyText`), not the output: a long command ellipsizes in the row, so the control is its only complete source, while output is selectable text below. Supplying `copyText` also keeps the control rendered before any output exists.
- **The panel scrolls instead of folding**: commands and output lines wrap in full (`--dsl-terminal-command-whitespace` / `--dsl-terminal-line-whitespace: pre-wrap`, so the output region never scrolls sideways) and the output region caps at a fixed height (`--dsl-terminal-output-max-height`) with `maxLines: Infinity` disabling the head/tail fold.
- **The panel draws no run-state dot** (`TerminalBlock.runStateDot: false`) — the row above it already carries the same state — and reclaims the dot gutter via `--dsl-terminal-gutter`. The panel's left spine was dropped with it.
All four land as opt-ins on `TerminalBlock` (props or `--dsl-terminal-*` variables), so tool cards keep their shipped fold/ellipsis/dot behavior unchanged.
## Testing
Component suites cover the join (job lifecycle wins, activity supplies the panel, bare-job rows, standalone activities), sections, durations, and the panel options; `pnpm vitest run packages/client/ui-activity packages/client/ui-primitives packages/client/ui-subagent` runs them. The keyless web e2e pair (`background-job-list`, `live-activity-stream`) replays the merged list end to end against ARIA goldens; `agent-preset-selection` pins the header band order.
## Consequences
- The two web e2e scenarios (`background-job-list`, `live-activity-stream`) now assert one "Tasks" list; the job-list scenario's row gained a live-output panel, which is the join working end to end.
- A human kill control on live rows is still blocked on the jobs `reported` contract question recorded in the job-display note.
- The `job` locale namespace is gone with its package; `activity` owns the merged copy including the duration vocabulary.
English | [中文](2026-09-11-independent-libreoffice-package.zh.md)
## Problem
Compiling LibreOffice inside ordinary DSH builds would require every contributor and CI job to acquire its toolchain and repeat a large native build. Desktop also needs immutable engine resources that can travel with its offline installation material.
## Decision
The [engine package](../../../../native/libreoffice-wasm/package.json) has its own version and an asset-only manifest export. Its source directory stays outside the main pnpm workspace. Installation has no lifecycle hook, and packaging verifies an existing successful build without compiling. The [release workflow](../../../../.github/workflows/libreoffice-wasm-release.yml) compiles only on explicit dispatch; publication requires a matching engine version tag and the protected npm environment.
Each tarball contains the engine manifest and assets, corresponding source pins, patches, full source diff, and the built distribution's license and notices. Packaging rejects mismatched build receipts, modified assets, unlisted engine files, missing notices, and bundled fonts. A failure never triggers compilation automatically.
The [local preview decision](../architecture/2026-09-10-local-office-preview.md) continues to own conversion and explicit artifact configuration. This change establishes release machinery; it does not add an unpublished engine dependency to DSH. The existing [Desktop package set](../../../../apps/desktop/scripts/prepare-package-set.ts) remains the intended carrier for a future fixed-version dependency and its offline seed.
## Alternatives considered
**Compile during ordinary builds or package installation.** This puts engine toolchain availability and compilation cost on every consumer, even when the engine version has not changed.
**Extract a general Node conversion library.** Font handling, Worker ownership, and the conversion API can remain with the provider. Moving them is unnecessary to distribute precompiled assets or isolate compilation from DSH CI.
**Link the engine source as a workspace dependency.** Workspace linking would replace a published precompiled package with a source directory that lacks its engine files.
## Consequences
The engine can be compiled and versioned independently of DSH. The package exposes files rather than a conversion API; the provider retains runtime ownership. Ordinary builds remain free of LibreOffice compilation, as checked by the [packaging tests](../../../../scripts/libreoffice-package.spec.ts).
Initial publication and fixed-version consumer integration remain incomplete. A source tree that differs from its recorded recipe cannot produce a release tarball. Real-engine conversion, installation from the packed package, and Desktop offline seed qualification must accompany the first consumed engine release.
@@ -10,7 +10,7 @@ The harness needs one internal language for messages that the loop, session log,
## Decision
Own the vocabulary: messages are arrays of typed content blocks (`text`, `reasoning`, `tool-call`, `tool-result`), with the union derived from the merge-extensible `ContentBlockMap` so plugins add block types via declaration merging. The same merge-extensible-map pattern types every "stringly" field (`MessageSource`, `FinishReason`, `TurnTrigger`, `TurnEndReason`). Streaming is a raw chunk protocol; `BlockAssembler` is the single shared assembly implementation. Adapters translate to provider wire formats — mapping cost lives in adapters, where it belongs.
Own the vocabulary: user, assistant, and system messages are arrays of typed content blocks (`text`, `reasoning`, `tool-call`), with the union derived from the merge-extensible `ContentBlockMap` so plugins add block types via declaration merging. Tool results are first-class tool-role messages whose own content holds result blocks; released Session format v3 retains the historical `tool-result` content block inside a user-role wrapper, and the adjacent V3-to-V4 edge lifts that wrapper into the first-class role. The same merge-extensible-map pattern types every "stringly" field (`MessageSource`, `FinishReason`, `TurnTrigger`, `TurnEndReason`). Streaming is a raw chunk protocol; `BlockAssembler` is the single shared assembly implementation. Adapters translate to provider wire formats — mapping cost lives in adapters, where it belongs.
In-session context injection (`context/message`) and mid-turn steering originally rendered as tagged user-role envelopes (the system-reminder pattern) rather than a new role, so adapters carry zero burden. Both now project as plain user content with no wrapper; see [the injected-content-envelope Agent Note](../simplification/2026-07-20-unwrap-injected-content-envelopes.md). Live-adapter validation confirms this rendering for current DeepSeek behavior; a future provider-specific mismatch belongs in that adapter rather than a new canonical role.
@@ -22,7 +22,7 @@ Responsibility is split between an always-on storage boundary and optional devel
`Session` accepts an event only after one recursive pass has materialized a lossless JSON snapshot. That pass rejects unsupported values and produces the exact detached record that enters the log, so validation and storage cannot observe different values from a stateful getter or retain caller-owned nested references.
The accepted event and all of its descendants are deep-frozen before publication. `append()` returns that owned frozen event, and `session/event` observers and `eventAt(seq)` receive the same record. `snapshotEvents(fromSeq?, toSeqExclusive?)` returns a frozen array snapshot; a previously returned array does not grow after a later append. `seq`and `eventAt()` avoid array materialization when a caller needs only the current length or one event. Seed records pass through the same validation, snapshot, and freeze boundary before construction succeeds.
The accepted event and all of its descendants are deep-frozen before publication. `append()` returns that owned frozen event, and `session/event` observers and `eventAt(seq)` receive the same record. `snapshotEvents(fromSeq?, toSeqExclusive?)` returns a frozen array snapshot; a previously returned array does not grow after a later append. `seq`reads the current length without materializing an array. Synchronous historical readers are deprecated under the [event-read policy](2026-09-09-deprecate-synchronous-session-event-reads.md). Seed records pass through the same validation, snapshot, and freeze boundary before construction succeeds.
This guarantee belongs in `Session`, not in an optional listener, because every composition relies on trustworthy history. A production deployment, a focused test, or a custom embedding receives the same storage semantics whether or not development support plugins are registered.
@@ -53,7 +53,7 @@ Detaching `deriveMessages()` would protect the most common request path but leav
## Consequences
- Every accepted live or seeded session event is detached from caller-owned inputs and deeply immutable before any observer can receive it.
-`snapshotEvents()`exposes stable immutable snapshots instead of the private growing array; `seq` and `eventAt()` serve scalar reads without copying that array.
- Existing`snapshotEvents()`and `eventAt()` callers retain immutable read results while their migration is deferred; `seq` reads the log length without copying the array.
- Request-side mutation cannot reach stored history through derived messages.
- Development builds can enable relational assertions without changing storage behavior, and disposing or filtering a companion does not weaken log immutability.
-`dsh-invariants` configures global enablement plus package allow/block regex lists; each check remains owned and tested by its product package.
@@ -8,7 +8,7 @@ English | [中文](2026-06-13-capability-seams.zh.md)
The harness has swappable capabilities, including shell execution and model providers. A capability has three concerns that change at different rates and for different reasons: the *contract* (what the capability is), the *implementation* (how it runs), and the *consumer API* (what the model and other plugins program against). Bundling them in one package couples those rates of change — swapping a local executor for a sandboxed one would churn the tool schemas the model sees, even though the model-facing contract never changed.
This is distinct from "who provides vs. needs a capability at runtime", which Cordis already answers with services + `inject` (a provider registers `ctx.shell`; a consumer declares `inject: ['bash']` and its fiber pends until the service exists). That mechanism is necessary but doesn't dictate package boundaries; this Agent Note does.
This is distinct from "who provides vs. needs a capability at runtime", which Cordis already answers with services + `inject` (a provider registers `ctx.shell`; a consumer declares `inject: ['shell']` and its fiber pends until the service exists). That mechanism is necessary but doesn't dictate package boundaries; this Agent Note does.
@@ -25,3 +25,5 @@ The rule they enforce: **anything the StreamChunk vocabulary cannot express for
## Consequences
The twin doubles adapter and key-gated e2e maintenance—both cover V4 Flash and Pro across representative reasoning modes—in exchange for continuous seam-neutrality validation and a second implementation example. Both use `apiKey`, `baseURL`, and `models`; the direct-fetch adapter exposes `thinking`/`reasoningEffort`, while pi-ai exposes one `reasoning` level. A future conformance suite could justify retiring one adapter through a superseding Agent Note.
The [Messages adapter](../feature/2026-09-07-deepseek-messages-adapter.md) adds an Anthropic-protocol implementation inside `llm-deepseek`; it preserves the same stream conventions.
@@ -37,7 +37,7 @@ Delta processing is O(1) when no new events and O(new events) when new events ar
### Persistence
The fields are serialized as top-level JSON properties. JSONL preserves placement and provenance without a separate column mapping. The [V3 canonical-envelope decision](2026-09-06-v3-canonical-session-envelopes.md) owns exact replacement keys and strict-acceptance rationale; the [V2-to-V3 specification](../../../../packages/session/session-format-v2-to-v3/README.md#canonical-envelopes) owns historical conversion. This note retains ordered-projection ownership and replacement rationale.
The fields are serialized as top-level JSON properties. JSONL preserves placement and source-event references without a separate column mapping. The [V3 canonical-envelope decision](2026-09-06-v3-canonical-session-envelopes.md) owns exact replacement keys and strict-acceptance rationale; the [V2-to-V3 specification](../../../../packages/session/session-format-v2-to-v3/README.md#canonical-envelopes) owns historical conversion. This note retains ordered-projection ownership and replacement rationale.
@@ -27,7 +27,7 @@ The literal types live on the [tasks subsystem page](../../../../docs/subsystems
The process-local provider also owns bounded admission, whose rationale is recorded in the [bounded background job admission decision](../../archived/bug-fix/2026-08-11-bounded-background-job-admission.md). Its positive-safe-integer `maxConcurrentJobsPerOwner` config defaults to `10`; `start()` derives each exact `Agent` object's active count from `running` and `stopping` records, while every unowned task shares one service bucket. Capacity rejection occurs before `run()` and id allocation, and producer `done` settlement is the only event that releases a stopping task's place. The provider does not queue, preempt, or retain a second mutable count.
`outputLimitBytes` is producer-owned presentation policy, not a registry buffer. The registry validates and projects it unchanged into `JobSnapshot`; generic control APIs apply the cap to complete model-facing output after adding their own status or notice metadata. Omitting it preserves the existing controller behavior, so the runtime does not impose a hidden default on unrelated producer families.
`outputLimitBytes` is producer-owned presentation policy, not a registry buffer. The registry validates and projects it unchanged into `JobView`; generic control APIs apply the cap to complete model-facing output after adding their own status or notice metadata. Omitting it preserves the existing controller behavior, so the runtime does not impose a hidden default on unrelated producer families.
A model-facing producer exposes that committed id in its canonical success value, normally `{ kind: 'background', jobId }`; Native rendering may keep human-readable prose. A pre-aborted background call fails rather than returning a no-op because no task exists to satisfy the promised handle. Once registration publishes the id, cancellation belongs to the task's own controller and the job runtime: later cancellation of the producing tool call must not kill the published task. `job_kill`, owner disposal, and service teardown request cancellation; foreground execution remains coupled to the call's `exec.signal`.
@@ -62,7 +62,7 @@ For contract-compliant producers, `AgentHandle.dispose()` resolves only after ow
-`read(id, caller?)` for a consuming stream delta or an idempotent final result.
-`kill(id, caller?, reason?)` for cancellation.
-`wait(id, timeoutMs, caller?, signal?)` for bounded terminal waiting.
-`onJobDone(listener)` for effect-scoped observation with exact-owner delivery and listener containment.
-`events.subscribe(filter, listener)` for effect-scoped observation of registration, progress, output, settlement, and removal, filtered by owner or owner scope, with listener containment.
-`attachController(name)` for the task-controller availability fence.
`wait` returns the terminal snapshot when the task settles or the live snapshot when its timeout expires. Aborting a wait cancels only that wait. If settlement has already assigned terminal delivery to the waiter, the terminal snapshot still wins. Waiters unregister synchronously on abort so a same-tick settlement cannot suppress a completion notice on behalf of a reader that receives nothing.
@@ -81,7 +81,7 @@ Stream reads share one task-scoped consuming cursor because the owning model is
The system prompt tells the model to retain job ids, continue independent work instead of busy-polling or duplicating a running task, collect relevant tasks before its final answer, and kill work that no longer matters. Completion delivers a logged message to the exact owner's session. A busy owner is injected; an idle owner is woken, under the bounded policy the [idle-owner wake decision](../feature/2026-08-11-background-job-completion-wakes-an-idle-owner.md) owns.
The runtime marks a terminal task `reported` when a read or wait delivers it, when a live waiter has claimed delivery at settlement, or when the model explicitly kills it. Reported tasks do not inject redundant completion notices. Listener failures are logged independently, do not stop later listeners, and are not awaited by waiters or teardown. When a snapshot carries `outputLimitBytes`, `dsh-tool-jobs` preserves UTF-8 boundaries and reuses an existing producer truncation marker rather than duplicating it. Reads reserve status suffixes and retain the output tail; completion notices reserve the stable `background job <id>` prefix and `job_output` instruction before truncating variable kind, label, status, detail, or the truncation marker itself, so the minimum PTY cap still identifies the task to collect. The job controller resolves the caller-visible producer cap in a prepended pre-execute listener before policy can deny or short-circuit dispatch, then applies it through the task definitions' last-mile `finalizeContent` callback so normalized tool errors, outer pipeline failures, and single-text policy results cannot escape the bound; deliberately structured multi-block policy results retain policy ownership of their shape and size.
`dsh-tool-jobs` keeps the delivery ledger: a wait claims the task when it starts (and withdraws if the task outlives the wait) and a `job_kill` claims it when accepted, so a settlement the model already received through a tool result injects no redundant completion notice; teardown settlements and unowned tasks are never announced ([seam consolidation](2026-09-03-jobs-seam-consolidation.md)). Listener failures are logged independently, do not stop later listeners, and are not awaited by waiters or teardown. When a view carries `outputLimitBytes`, `dsh-tool-jobs` preserves UTF-8 boundaries and reuses an existing producer truncation marker rather than duplicating it. Reads reserve status suffixes and retain the output tail; completion notices reserve the stable `background job <id>` prefix and `job_output` instruction before truncating variable kind, label, status, detail, or the truncation marker itself, so the minimum PTY cap still identifies the task to collect. The job controller resolves the caller-visible producer cap in a prepended pre-execute listener before policy can deny or short-circuit dispatch, then applies it through the task definitions' last-mile `finalizeContent` callback so normalized tool errors, outer pipeline failures, and single-text policy results cannot escape the bound; deliberately structured multi-block policy results retain policy ownership of their shape and size.
## Producer opt-in
@@ -105,7 +105,7 @@ Separate bash and subagent output/stop tools duplicate ids, isolation, cleanup,
### An immediate abstract task-runtime backend
The current `JobStart.run()` contract passes in-process callbacks and exact `Agent` objects. A durable backend changes identity, restart, ownership, and observation semantics, so at introduction time the registry stayed one concrete service rather than freezing the wrong boundary. The [task-registry contract Agent Note](../../archived/architecture/2026-07-26-job-registry-seam.md) later separated the contract from the process-local implementation without changing these in-process semantics.
The current `JobSpec.run()` contract returns in-process hooks and pushes output through an in-process `JobHandle`, and owners resolve to live `Agent`s in-process. A durable backend changes identity, restart, ownership, and observation semantics, so at introduction time the registry stayed one concrete service rather than freezing the wrong boundary. The [task-registry contract Agent Note](../../archived/architecture/2026-07-26-job-registry-seam.md) later separated the contract from the process-local implementation without changing these in-process semantics.
### Consumer-owned authorization or cleanup events
@@ -26,7 +26,7 @@ The assembled system prompt had four defects, all of one family: facts the harne
### Prompt variables
Plugins register `{{name}}` values through `ctx.systemPrompt.variable(name, provider)`. Assembly resolves them into the waterfall-visible variable map. Rendering rejects unknown own-property references, registered providers that return `undefined`, malformed complete references, and unbalanced references that still contain a closing `}}`; a lone unmatched `{{` remains prose, and substituted values are not rescanned. Registration rejects invalid or duplicate variable names, and section names are unique.
Plugins register `{{name}}` values through `ctx.systemPrompt.variable(name, provider)`. Assembly resolves them into the waterfall-visible variable map. Rendering rejects unknown own-property references, registered providers that return `undefined`, malformed complete references, and unbalanced references that still contain a closing `}}`; a lone unmatched `{{` remains prose, and substituted values are not rescanned. Registration rejects invalid or duplicate variable names, and section names are unique. Sections may set `interpolate: false` to preserve generated documents literally; `tools:sdk` does so because tool descriptions and schemas may document their own `{{…}}` syntax.
`dsh-agent-loop` registers the two built-ins, both pure projections of the context agent: `model` (= `options.model`) and `cwd` (= `session.header.cwd`). The example personas write `powered by the {{model}} model` — the model name is stated once, in the `model:` config key. `{{cwd}}` is demonstrated in the ACP example only: every ACP session carries the client's cwd, while config-pre-created stdio agents have none (a persona claiming `{{cwd}}` there fails the turn — by design). The variables stay on the loop plugin (unlike the sections below): they are runtime facts of the agents THIS loop drives, and a replacement loop supplies its own.
@@ -47,7 +47,7 @@ Per-tool semantics and selection guidance live in tool descriptions. Prompt sect
- **The loop composes an identity line itself** — hardcodes model-facing prose in the one package that must stay thin ("plugins, not loop changes"), and outside the section pipeline it would be a second composition path. (The identity DOES ship as a code literal — but as an ordinary section registered by `dsh-system-prompt`, whose `system-prompt/assemble` waterfall remains the escape valve for a deployment that must drop it.)
- **Inject the model name via the `agent/request` waterfall** — prompt text would be composed in two places and the earlier rendered persona could disagree with the final routed header. The request plugin that owns late routing must also own any earlier prompt claim about that model.
- **Hand-write the model name in each persona** — duplicates the `model:` key one line above and silently lies after a config edit; the exact disease this decision cures.
- **Lenient interpolation (leave unknown refs verbatim, or substitute empty)** — a typo ships `{{modle}}` (or a hole) to the model and nobody notices until transcript review.
- **Lenient interpolation (leave unknown refs verbatim, or substitute empty)** — a typo ships `{{modle}}` (or a hole) to the model and nobody notices until transcript review. Leaving only unknown names unchanged would still substitute registered names inside tool documentation.
- **Per-instance subagent wording in config** — returns model-facing prose to every deployment × instance, reviving the hand-written-guidance-in-leaf-YAML drift. **Keying wording off the provider NAME** — `providerName` is itself config, so a renamed provider silently gets the wrong words.
- **Resolving the provider at `apply` time (a load-order requirement)** and **section-only subagent wording (lazily resolved at assemble)** — the alternatives to the provider-lifecycle events; both rejected in [the provider-lifecycle-events Agent Note](../../archived/architecture/2026-07-05-subagent-provider-lifecycle-events.md).
@@ -60,7 +60,7 @@ Per-tool semantics and selection guidance live in tool descriptions. Prompt sect
- The tui-agent prompt renders identity, persona with the interpolated model, then fs/shell/web guidance through one assembly path.
- Fork and fresh subagent descriptions reflect whether the provider inherits completed conversation turns; the tool appears, disappears, and is reworded with provider lifecycle changes.
-Unknown, valueless, malformed, or unbalanced variable references name the section and throw; duplicate section, variable, and tool registrations also throw.
-In interpolated sections, unknown, valueless, malformed, or unbalanced variable references name the section and throw; duplicate section, variable, and tool registrations also throw.
- Snapshot replay is prompt-independent: it keys recorded chunk streams by turn and step without comparing the outgoing request.
## Consequences
@@ -69,4 +69,4 @@ Per-tool semantics and selection guidance live in tool descriptions. Prompt sect
-`{{model}}` reflects `AgentOptions.model` at assembly time. A plugin that switches models in the `agent/request` waterfall makes the prompt's claim stale for that step, and one that SUPPLIES the model there (options.model unset — the loop's documented fallback) leaves the variable valueless at render, failing a `{{model}}` persona before the waterfall runs. Both have the same remedy, and it is the ownership rule itself: the plugin that owns the late-bound model fact states it early on the `system-prompt/assemble` waterfall (`assembly.variables['model'] = …`) — one owner, both statements; a loop test pins the supply path end-to-end. Accepted.
- While a bound provider is absent (not yet activated, unloaded, mid-HMR-reload), the subagent tool does not exist and a model request in that window simply lacks it. That is the honest state — the alternative was a registered tool whose description or execution could not be trusted.
- Strictness means a persona can fail a turn at render (e.g. `{{cwd}}` on a cwd-less session). The failure is contained — the turn ends `error`, the loop survives — and it is an authoring error we WANT loud.
-No escape syntax for a literal `{{name}}` in prompt prose yet; add one if a real prompt ever needs it.
-Inline escapes remain unsupported in interpolated text; literal sections need no escaping. PTC unit tests cover both modes and runtime languages, and the recorded `ptc-turn` scenario preserves tool-template examples in the model-visible prompt.
@@ -22,9 +22,9 @@ Prefix-cache stability is corollary #1, not the headline: an append-only log pro
**Messages.**`Session.deriveMessages()` is cached: each surface entry is projected exactly once, when first seen, through the public per-event function `deriveEventMessage(event)`; a surface rewrite (a compaction `replace` — `SurfaceManager.replaceGeneration`) rebuilds. Callers get a fresh array per call over shared, deep-frozen messages: mutating logged history through a projection is unrepresentable (it throws), replacing the old clone-per-call isolation. External reconstructors fold the same public function over a log prefix, so no two paths can disagree.
`EpochHeader` records the request's non-history state: call config and tool schemas. Writers omit `tools: []` and `adapterDefaults: {}`; current acceptance rejects those fields and any `header.system`, rather than repairing them. Whitespace-only system-message content, `config.stop: []`, and nested extensions remain intact. The [V3 canonical-envelope decision](2026-09-06-v3-canonical-session-envelopes.md) owns historical conversion. The rendered system prompt is derived history — the `system/message` event at surface node 0, per the [surface-node Agent Note](2026-09-02-system-prompt-as-surface-node.md) — so a prompt change is a surface replacement rather than a header change. Adapter-supplied effort and token defaults retain their `adapterDefaults`provenance; a Web model selection restored from the log omits an adapter-owned effort so the next resolution cannot reclassify the same effective config as an explicit selection and a false change. `request/header` always writes a full snapshot: the first loop instance uses reason `initial`, later instances use `resume`, an in-instance change uses `change`, and an unchanged envelope beginning an explicitly declared message series or following a surface replacement uses `series`. A `change` snapshot carries `startsSeries: true` when the changed request also starts a series, preserving the two independent facts without a duplicate header. Ordinary append-only later Turns, further same-series Steps, and retries inherit the latest snapshot. `foldRequestHeader` selects the latest snapshot. Legacy `request/header-delta` events and the removed `fallback` reason are rejected when appended or loaded.
`EpochHeader` records the request's non-history state: call config and tool schemas. Writers omit `tools: []` and `adapterDefaults: {}`; current acceptance rejects those fields and any `header.system`, rather than repairing them. Whitespace-only system-message content, `config.stop: []`, and nested extensions remain intact. The [V3 canonical-envelope decision](2026-09-06-v3-canonical-session-envelopes.md) owns historical conversion. The rendered system prompt is derived history — the `system/message` event at surface node 0, per the [surface-node Agent Note](2026-09-02-system-prompt-as-surface-node.md) — so a prompt change is a surface replacement rather than a header change. Adapter-supplied effort and token defaults retain their `adapterDefaults`ownership; a Web model selection restored from the log omits an adapter-owned effort so the next resolution cannot reclassify the same effective config as an explicit selection and a false change. `request/header` always writes a full snapshot: the first loop instance uses reason `initial`, later instances use `resume`, an in-instance change uses `change`, and an unchanged envelope beginning an explicitly declared message series or following a surface replacement uses `series`. A `change` snapshot carries `startsSeries: true` when the changed request also starts a series, preserving the two independent facts without a duplicate header. Ordinary append-only later Turns, further same-series Steps, and retries inherit the latest snapshot. `foldRequestHeader` selects the latest snapshot. Legacy `request/header-delta` events and the removed `fallback` reason are rejected when appended or loaded.
Each proposed step first claims its inbox batch, assembles the system prompt and tools, projects the rendered prompt against the surviving `system/message` node, and runs `agent/pre-step`. Rejection opens no step; enter opens `step/start`, commits a changed prompt as the `system/message` append or node-0 replacement, records the final message batch as `user/message` events, and may use `startsRequestSeries: true` to declare a distinct series. `agent/request` may replace only the frozen call-config seed. The loop records the owed initial, resume, change, or series full snapshot, builds `GenerateOptions` from derived messages (system message first) and that header with no `system` field, and freezes it while leaving `AbortSignal` live. The [request-freeze provenance decision](../simplification/2026-09-06-agent-request-freeze-provenance.md) owns reuse of completed message freezes and per-request local header freezing. The first call config starts from explicit `AgentOptions`, preserving fork overrides and resume reconfiguration; later calls start from the folded header.
Each proposed step first claims its inbox batch, assembles the system prompt and tools, projects the rendered prompt against the surviving `system/message` node, and runs `agent/pre-step`. Rejection opens no step; enter opens `step/start`, commits a changed prompt as the `system/message` append or node-0 replacement, records the final message batch as `user/message` events, and may use `startsRequestSeries: true` to declare a distinct series. `agent/request` may replace only the frozen call-config seed. The loop records the owed initial, resume, change, or series full snapshot, builds `GenerateOptions` from derived messages (system message first) and that header with no `system` field, and freezes it while leaving `AbortSignal` live. The [request-freeze evidence decision](../simplification/2026-09-06-agent-request-freeze-evidence.md) owns reuse of completed message freezes and per-request local header freezing. The first call config starts from explicit `AgentOptions`, preserving fork overrides and resume reconfiguration; later calls start from the folded header.
**The open step is the reconstruction boundary.** Its entered `user/message` batch and any newly written `request/header` precede request dispatch. Injection after the atomic claim joins a later request, while a listener that must affect this request returns messages through `agent/pre-step`. Header reconstruction selects the step's `request/header`, or carries the prior snapshot when no new header is written.
@@ -22,7 +22,7 @@ A thin spill storage seam plus a default spill policy plugin, in a new `packages
| `@deepseek-ai/dsh-spill-local` | Local backend: private, session-scoped file storage on the host filesystem. |
| `@deepseek-ai/dsh-spill-policy` | Tool-result policy plugin: wraps final text results after dispatch and replaces oversized results with a retained preview plus a spill locator. |
The tool-result Consumer is `dsh-spill-policy`, which consumes final tool results through the `tools/post-execute` waterfall. The model follows the backend-supplied retrieval hint for the returned locator. [Session-reference spill reuse](../bug-fix/2026-09-05-session-reference-spill-reuse.md) adds a direct storage consumer with separate preview, provenance, and failure semantics; it does not change the tool-result policy.
The tool-result Consumer is `dsh-spill-policy`, which consumes final tool results through the `tools/post-execute` waterfall. The model follows the backend-supplied retrieval hint for the returned locator. [Session-reference spill reuse](../bug-fix/2026-09-05-session-reference-spill-reuse.md) adds a direct storage consumer with separate preview, source-description, and failure semantics; it does not change the tool-result policy.
### Spill seam
@@ -66,36 +66,7 @@ interface SpillRef {
### Spill policy
`dsh-spill-policy` is a `tools/post-execute` result transformer with one configuration knob:
```ts ignore-check
interface Config {
/** Omitted means no automatic spill policy. Present means apply to oversized plain text tool results. */
maxInlineBytes?: number
}
```
When `maxInlineBytes` is omitted the plugin registers nothing (a true no-op). When set, it applies a default policy to final plain-text tool results:
1. Let the tool run normally, delegating via `next()` so a downstream listener settles the result first.
2. Flatten the accepted final `ContentBlock[]` only when it is entirely plain text; a result with any non-text block is left untouched.
3. If its UTF-8 byte size is at or below `maxInlineBytes`, leave it unchanged.
4. If it is larger, call `ctx.spillStore.saveText()` with the full final text.
5. Replace the model-facing result with a retained head/tail preview plus the spill reference.
The preview is an implementation default owned by the policy: a head/tail split of `maxInlineBytes` via the retention library's `TextRetainer`. Future config can expose preview sizing only after a second deployment needs it.
The replacement text is intentionally generic because the policy only knows the final formatted tool result, not the tool's internal resource:
```text
<retained preview>
(Omitted N bytes. Full formatted result stored at: /.../session-.../....txt. Use read with offset/limit, or grep this path to search within it.)
```
If `ctx.spillStore.saveText()` fails (permissions, ENOSPC, backend unavailable), or the call has no session owner, or no backend is loaded, the plugin logs the reason and returns the original result unchanged. Spill failure never turns a successful tool call into an `isError` result or hides the inline result.
The policy skips `read` to avoid a circular `read -> spill file -> read again` loop. Additional opt-out configuration is deferred until a real second tool needs it.
`dsh-spill-policy` retains ordered text/image ends under `maxInlineTokens` after post-execute policy accepts the result. `saveText()` stores the complete result with readable image paths; image bytes remain in attachment storage. The [multimodal retention decision](../../implemented/bug-fix/2026-09-21-multimodal-tool-result-retention.md) owns image projection order, atomic image omission, and route pricing. An omitted budget installs no listeners, recovery failures keep the original result, and model-facing `read` results skip retention to avoid a read/spill loop.
## Showcase: web_fetch
@@ -129,7 +100,7 @@ With `dsh-spill-policy` configured, a large formatted fetch result is automatica
- id: spill-policy
name: '@deepseek-ai/dsh-spill-policy'
config:
maxInlineBytes: 50000
maxInlineTokens: 12500
```
This separation is important. `web-fetch-http` still owns resource caps (`maxResponseBytes`, `maxBodyChars`) to protect network, memory, and decoding work. `spill-policy` owns only the model-facing context cap after the result already exists. If the provider already returned `truncated: true`, the spill file contains the full formatted result the tool returned, not the full original webpage; the policy does not claim otherwise.
@@ -186,11 +157,11 @@ The local-backend value proposition depends on the existing `read`/`grep` tools
**Snapshot gap.** No ACP snapshot scenario covers the transcript-visible `web_fetch` spill notice yet. The ACP snapshot harness replays keyless and cannot hit the live web, and a `web_fetch` spill requires a real over-cap HTTP body; a deterministic scenario would need a seeded loopback fetch target the replay tree does not currently wire (the examples do not load `tool-web` at all). The behavior is covered instead by the `dsh-tool-web` integration test against a loopback server. Closing the gap is follow-up work: wire `tool-web` + a seeded fetch target into the ACP example, then record a `web-fetch-spill` scenario.
The policy can become too large if it starts owning tool-specific semantics. It stays narrow: plain-text final results only. Tool-owned early spill remains future work.
The policy can become too large if it starts owning tool-specific semantics. It handles accepted text/image sequences without interpreting tool-specific text. Tool-owned early spill remains future work.
## Alternatives considered
**Require each tool to opt in with a retention declaration.** Rejected: the goal is a default behavior similar to Claude Code's generic tool-result persistence. A single `maxInlineBytes` deployment knob is enough to prove the shape.
**Require each tool to opt in with a retention declaration.** Rejected: the goal is a default behavior similar to Claude Code's generic tool-result persistence. A single `maxInlineTokens` deployment knob is enough to prove the shape.
**Make `tool-results` a broad tool-result platform.** Rejected: a broad package name invites retention policy, result replacement, preview wording, search, and early spill into one seam. The shared storage part is smaller: save text and return a locator plus retrieval hint.
@@ -23,6 +23,8 @@ The exe is packaged with the **`--sea` (enhanced SEA) mode** of [@yao-pkg/pkg](h
`@yao-pkg/pkg` is an exact-pinned root `devDependency` invoked as `pnpm exec pkg`, with [`patches/@[email protected]`](../../../../patches/@[email protected]) removing the SEA bootstrap's `patchChildProcess` call. Unpatched, pkg rewrites spawned commands named `node` — including the string after a `-c`/`/c` flag, exactly the Bash tool's `bash -c` form — to the executable itself and stamps `PKG_EXECPATH` into every child environment, so a model-issued `node --version` silently boots the dsh CLI; Node's own SEA layer performs no such rewrite, and a SEA binary cannot impersonate plain Node because it always boots its embedded app. With the call removed, children resolve `node` through PATH like any other process (a machine without Node reports command-not-found honestly), no `PKG_EXECPATH` reaches children, absolute `process.execPath` spawns still re-enter the app, worker threads never applied the hook, and `process.pkg` sidecar selection is untouched.
The same patch makes the SEA bootstrap's native-addon extraction atomic. On the first `dlopen` of an addon inside the VFS, pkg's `patchDlopen` copies the addon's package tree to `$PKG_NATIVE_CACHE_PATH/pkg/<sha256 of the .node file>/` (default `~/.cache/pkg/...`), a per-user directory that every concurrent process of the same executable shares; upstream writes each file straight to its final path with `writeFileSync`, which truncates a file another process may already have mapped and lets that process's `existsSync`/hash check read a half-written file. Concurrent first starts therefore die with SIGBUS on Linux or fail with `ERR_DLOPEN_FAILED: Library not loaded` on sharp's libvips on macOS (#4664). The patch routes the three write sites through a same-directory temporary file and `renameSync`, so a cache path is either absent or complete, every process finishes its own copy pass before it loads, and concurrent renames of identical content need no lock. The cache location, hash keying, and per-file comparison are unchanged, so a consumer that isolates the cache per instance through `PKG_NATIVE_CACHE_PATH` keeps working; worker threads only define the hook and never install it. Measured (linux-x64, node24 target, pkg 6.21.0, a probe that loads sharp with its 18 MB libvips library, 16 concurrent first starts against one empty cache, five rounds): 39 of 80 processes died with SIGBUS before the change and 0 of 80 after it, with no temporary files left behind.
Terminology reminder: pkg's `/snapshot` VFS has nothing to do with this repo's testing-system "snapshot" (ACP replay expected outputs, `$DSH_SNAPSHOT`); this document says "VFS" for the former.
### The serving interface is a plugin inside the dsh application
@@ -38,21 +40,23 @@ The Python client supplies an explicit Harness home and selects the `sdk` profil
Inside the exe's VFS sits a **real package tree in build-artifact form** (each package's `lib/` plus a real `node_modules`). The packaged JSON-RPC entry supplies its installed harness base to app-boot's root Include: relative plugin specifiers resolve from the external configuration directory, while bare package names resolve from the VFS, so a configuration inside another Node project cannot shadow the packaged plugin set. The ordinary development bin leaves bare packages configuration-owned. Bare specifiers in the packaged entry resolve upward along `node_modules` from the entry's position inside the VFS and land inside the VFS naturally. The closed set needs no allowlist code — the set is whatever the VFS has installed, and importing a name outside the set fails.
The deploy root is [`python/sdk-runtime/package.json`](../../../../python/sdk-runtime/package.json) (`dsh-python-runtime-closure`, a pnpm workspace member and a zero-code pure dependency manifest) — the unified source of truth for "which plugins the exe ships" and "what the Python runtime distributes". Adding a plugin to the exe = adding one dependency line to the manifest and repackaging. [`scripts/verify-runtime-closure.ts`](../../../../scripts/verify-runtime-closure.ts) reads every shipped `packages/preset/agent-presets/presets/*/agent.cordis.yml`, evaluates `disabled` conditions that compare `process.platform` for every target in `python/sdk-runtime/platforms.json`, and requires each active workspace plugin at the runtime root through an explicit `workspace:` dependency. It also traverses every workspace package covered by that manifest and requires every non-optional workspace peer, reporting the complete preset or referencing-package → missing-dependency chain; unknown platform conditions remain active so a plugin cannot be omitted by an unsupported expression. `pnpm run hygiene`, CI static, and the single-exe build run it before packaging. Deploy also packs by each package's `files`, so the shared chunks tsdown splits out must be covered by `files`.
The deploy root is [`python/sdk-runtime/package.json`](../../../../python/sdk-runtime/package.json) (`dsh-python-runtime-closure`, a pnpm workspace member and a zero-code pure dependency manifest) — the unified source of truth for "which plugins the exe ships" and "what the Python runtime distributes". Adding a plugin to the exe = adding one dependency line to the manifest and repackaging. [`scripts/verify-runtime-closure.ts`](../../../../scripts/verify-runtime-closure.ts) reads every shipped `packages/bundle/web-app/cordis.patch.yml*/agent.cordis.yml`, evaluates `disabled` conditions that compare `process.platform` for every target in `python/sdk-runtime/platforms.json`, and requires each active workspace plugin at the runtime root through an explicit `workspace:` dependency. It also traverses every workspace package covered by that manifest and requires every non-optional workspace peer, reporting the complete preset or referencing-package → missing-dependency chain; unknown platform conditions remain active so a plugin cannot be omitted by an unsupported expression. `pnpm run hygiene`, CI static, and the single-exe build run it before packaging. Deploy also packs by each package's `files`, so the shared chunks tsdown splits out must be covered by `files`.
The deploy root includes `@deepseek-ai/dsh-mcp-client` as an explicitly supported custom-configuration plugin even though no shipped preset mounts it. An external config can therefore connect to user-supplied stdio and Streamable HTTP MCP servers and register their tools; the distribution does not carry those servers or extend the bridge to MCP Resources and Prompts. The executable and installed-wheel smokes start a temporary stdio server, discover its tool, and complete one model-requested call.
The deploy root includes `@deepseek-ai/dsh-mcp-client` as an explicitly supported custom-configuration plugin even though no shipped preset mounts it. An external config can therefore connect to user-supplied stdio and Streamable HTTP MCP servers and register their tools; the distribution does not carry those servers. The optional resource service provides MCP Resources; MCP Prompts remain unsupported. The executable and installed-wheel smokes start a temporary stdio server, discover its tool, and complete one model-requested call.
The [Office kit](2026-09-14-independent-libreoffice-kit.md) and its installed dependency tree live in `<executable-stem>-office/node_modules`, preserving complete package contents and helper permissions. Top-level `pkg.ignore` excludes kit packages from the SEA archive, including assets selected by dependency `files` fields; small dependencies shared with other consumers remain available in the VFS. The private Python bootstrap uses Node `registerHooks` to resolve the kit entry and its package manifest from the real directory. Native executables and URL Workers therefore retain their ordinary paths without modifying the kit or the public CLI. Missing required packages and missing installed package manifests fail the copy; absent optionals preserve the kit's engine selection.
### Build pipeline and artifacts
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true`**directly into**`python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`, including [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the carrier-root `runtime-bootstrap.mjs` → restore direct workspace packages omitted by legacy deploy and reject any remaining manifest gap → replace staged dependency symlinks with their target bytes, remove package-manager `.bin` links, and fail if any symlink remains → verify the deployed bootstrap and inject pkg configuration with that bin plus assets covering dynamic profile, bundle, frontend, preset, native-library, and configuration reads → stage the target `node-pty` addon → invoke `pkg --sea` once per target → write `deepseek-harness-sdk-runtime-<platform>-<arch>` under `dist-exe/` and copy it into the runtime directory. The Python runtime owns that bootstrap. It calls the public CLI export for ordinary launches and dispatches a provider-private selection to the same `@deepseek-ai/dsh-subprocess-local/runner` core without changing CLI grammar or adding another executable; the [native-containment decision](2026-08-28-subprocess-native-containment.md) owns that private path. Linux CI rebuilds `pty.node` inside the matching manylinux 2.28 container because legacy deploy omits that install side effect. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted gives pkg a stable single-instance layout that the explicit materialization pass makes symlink-free; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry.
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-python-runtime-closure deploy --legacy --prod --config.allow-unused-patches=true --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true`**directly into**`python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/`, including [`python/sdk-runtime/runtime-bootstrap.mjs`](../../../../python/sdk-runtime/runtime-bootstrap.mjs) as the carrier-root `runtime-bootstrap.mjs` → restore direct workspace packages omitted by legacy deploy and reject any remaining manifest gap → replace staged dependency symlinks with their target bytes, remove package-manager `.bin` links, and fail if any symlink remains → verify the deployed bootstrap and inject pkg configuration with that bin plus assets covering dynamic profile, bundle, frontend, preset, native-library, and configuration reads → stage the target `node-pty` addon → invoke `pkg --sea` once per target → write `deepseek-harness-sdk-runtime-<platform>-<arch>` under `dist-exe/` and copy it with its target-specific Office directory into the runtime directory. The Python runtime owns that bootstrap. It calls the public CLI export for ordinary launches and dispatches a provider-private selection to the same `@deepseek-ai/dsh-subprocess-local/runner` core without changing CLI grammar or adding another executable; the [native-containment decision](2026-08-28-subprocess-native-containment.md) owns that private path. Linux CI rebuilds `pty.node` inside the matching manylinux 2.28 container because legacy deploy omits that install side effect. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. The deploy flags preserve the measured runtime layout: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted gives pkg a stable single-instance layout that the explicit materialization pass makes symlink-free; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry. Production deployment permits unused workspace patches because development-only tools can be absent from its closure; included-package patch failures still fail deployment. Workspace installs retain strict unused-patch validation.
CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml) runs [installed-wheel validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md) on Linux/Windows x64 for pull requests and Linux ARM64 plus both macOS architectures for master pushes. The [public publication workflow](../../archived/process/2026-08-11-python-publication-workflow.md) calls it for all five targets; `workflow_dispatch` can still select a subset. Native builds run on linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64 / macos-x64 (`macos-15-intel`) / win-x64 (`windows-2025`), with `~/.pkg-cache` cached where applicable, and pkg handles macOS ad-hoc signing. Each leg installs the release-shaped SDK and runtime wheels into a clean venv outside the checkout, proves their package and executable provenance, then drives the complete keyless scenario set through the public SDK and direct NDJSON JSON-RPC. Trusted pull requests and master pushes additionally run a real DeepSeek two-turn tool smoke on their selected targets; fork and Dependabot heads receive no key. Linux inspects the executable and native addon's GLIBC requirements and runs an additional manylinux 2.28 smoke, while macOS checks the runtime, ripgrep, and PTY helper architectures and verifies that all three deployment targets fit the wheel tag. A full five-target run retains six artifacts, each containing one release file: the platform-independent SDK wheel and five native runtime wheels; a subset dispatch retains the SDK wheel and selected runtime wheels. Bare executables and source bundles remain intermediate test inputs. [`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) accepts `python-v<repository-version>` tag pipelines whose version matches the root `package.json`, builds one SDK wheel and five native runtime wheels, then a single serialized job checks and publishes all six to the project PyPI registry. The [`python/sdk-runtime` README](../../../../python/sdk-runtime/README.md) owns the Windows target and the explicit exclusion of Windows arm64.
CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml) runs [installed-wheel validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md) on Linux/Windows x64 for pull requests and Linux ARM64 plus both macOS architectures for master pushes. The [public publication workflow](../../archived/process/2026-08-11-python-publication-workflow.md) calls it for all five targets; `workflow_dispatch` can still select a subset. Native builds run on linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64 / macos-x64 (`macos-15-intel`) / win-x64 (`windows-2025`), with `~/.pkg-cache` cached where applicable, and pkg handles macOS ad-hoc signing. Each leg installs the release-shaped SDK and runtime wheels into a clean venv outside the checkout, verifies their package and executable origin, then drives the complete keyless scenario set through the public SDK and direct NDJSON JSON-RPC. Trusted pull requests and master pushes additionally run a real DeepSeek two-turn tool smoke on their selected targets; fork and Dependabot heads receive no key. Linux inspects the executable and native addon's GLIBC requirements and runs an additional manylinux 2.28 smoke, while macOS checks the runtime, ripgrep, and PTY helper architectures and verifies that all three deployment targets fit the wheel tag. A full five-target run retains six artifacts, each containing one release file: the platform-independent SDK wheel and five native runtime wheels; a subset dispatch retains the SDK wheel and selected runtime wheels. Bare executables and source bundles remain intermediate test inputs. [`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) accepts `python-v<repository-version>` tag pipelines whose version matches the root `package.json`, builds one SDK wheel and five native runtime wheels, then a single serialized job checks and publishes all six to the project PyPI registry. The [`python/sdk-runtime` README](../../../../python/sdk-runtime/README.md) owns the Windows target and the explicit exclusion of Windows arm64.
### Python SDK distribution: two carriers, exe for production, node for development
The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` is the client and `python/sdk-runtime` is the runtime carrier package. The runtime package's data directory holds the build-injected platform executable with its required `-rg` sidecar and optional macOS helper, plus the build-injected `runtime/node/` closure tree for repository development. `resolve_bundled_launch_args()` selects the executable by default; explicit `DSH_RUNTIME_MODE=node` runs `runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js` on system Node 22.19 or newer. The node carrier never enters wheel distributions, and neither carrier uses a checked-in complete `cordis.yml`.
The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` is the client and `python/sdk-runtime` is the runtime carrier package. The runtime package's data directory holds the build-injected platform executable with its required `-rg` sidecar, `-office/` directory, and optional macOS helper, plus the build-injected `runtime/node/` closure tree for repository development. `resolve_bundled_launch_args()` selects the executable by default; explicit `DSH_RUNTIME_MODE=node` runs `runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js` on system Node 22.19 or newer. The node carrier never enters wheel distributions, and neither carrier uses a checked-in complete `cordis.yml`.
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative `X.Y.Z` or prerelease version from the repository root `package.json`, converts prereleases to their PEP 440 spelling, and stages both packages at that wheel version, with `deepseek-harness-sdk` depending exactly on the matching `deepseek-harness-runtime-bin`. An optional `python-v<repository-version>` release tag is a consistency assertion and is rejected when it differs from the repository version; the source `pyproject.toml` development sentinel never determines a release version. Staging also carries the repository license into both wheels and the third-party notices into the bundled runtime wheel. The SDK is a `py3-none-any` wheel; each wheel-only runtime package contains one exe and its architecture-matched ripgrep sidecar, and the macOS wheel also contains its architecture-matched spawn helper. Runtime wheels use `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, `py3-none-macosx_14_0_arm64`, `py3-none-macosx_14_0_x86_64`, or `py3-none-win_amd64`; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra sidecars, and unsupported platforms. Both macOS tags deliberately declare a conservative 14.0 installation floor: the packaged Node 24 executables declare macOS 13.5, and the x64 PTY helper declares 10.7, but release validation proves the complete payload only against the 14.0 wheel claim rather than promising each observed component minimum as a supported host. The two macOS wheels remain architecture-specific; no universal2 wheel is published.
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative `X.Y.Z` or prerelease version from the repository root `package.json`, converts prereleases to their PEP 440 spelling, and stages both packages at that wheel version, with `deepseek-harness-sdk` depending exactly on the matching `deepseek-harness-runtime-bin`. An optional `python-v<repository-version>` release tag is a consistency assertion and is rejected when it differs from the repository version; the source `pyproject.toml` development sentinel never determines a release version. Staging also carries the repository license into both wheels and the third-party notices into the bundled runtime wheel. The SDK is a `py3-none-any` wheel; each wheel-only runtime package contains one exe, its Office directory, and its architecture-matched ripgrep sidecar, and the macOS wheel also contains its architecture-matched spawn helper. Runtime wheels use `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, `py3-none-macosx_14_0_arm64`, `py3-none-macosx_14_0_x86_64`, or `py3-none-win_amd64`; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra sidecars, and unsupported platforms. Both macOS tags deliberately declare a conservative 14.0 installation floor: the packaged Node 24 executables declare macOS 13.5, and the x64 PTY helper declares 10.7, but release validation proves the complete payload only against the 14.0 wheel claim rather than promising each observed component minimum as a supported host. The two macOS wheels remain architecture-specific; no universal2 wheel is published.
The Python client launches the packaged `dsh` command with the selected profile (`sdk` by default), ordered patch files, and an explicit Harness home. The profile owns JSON-RPC serving and application composition; missing homes, profiles, bundles, patches, and server rows fail without an external complete-config fallback.
@@ -60,16 +64,18 @@ The Python client launches the packaged `dsh` command with the selected profile
`dsh-python-runtime-closure` is the private deploy manifest and `deepseek-harness-sdk-runtime-<platform>-<arch>` is the executable family. The wire `serverInfo.name` is `deepseek-harness-sdk-runtime`; the Python distribution names are `deepseek-harness-sdk` / `deepseek-harness-runtime-bin`, while the import modules are `deepseek_harness` / `deepseek_harness_runtime`.
## Disposition of worker-style plugins
## Packaged workflow and code execution
`dsh-workflow-worker-thread` and `dsh-code-runtime-worker-thread` are supported inside the exe. Their built hosts convert the sibling `lib/worker.cjs` URL with `fileURLToPath()` and pass the resulting filesystem string to `Worker`, which is the form pkg's Worker hook resolves inside the VFS. The worker entries are CommonJS because that hook compiles VFS worker files as CommonJS. The workflow engine keeps its data-URL bootstrap for unbuilt source execution; only its built sibling entry uses the filesystem string. The custom-config executable smoke loads both backends, invokes a real `run_code` call and a zero-agent `workflow` call, and requires each worker to return `42` from inside pkg's VFS.
`dsh-workflow-ptc` sends its self-contained guest program through `dsh-ptc-runtime-node`. The shared provider starts a managed child through the executable's private Node bootstrap dispatch, with a dedicated control channel; no separate workflow worker asset is required. The [sandboxed Node decision](2026-09-11-sandboxed-node-ptc-runtime.md) owns process and policy lifetime, and [workflow sandbox reuse](2026-09-13-workflow-ptc-sandbox-reuse.md) owns the workflow adapter.
## Testing
The verification surface has three tiers. Mechanism tier: the measured conclusions for the `--sea` chain are embedded in the Decision sections (ESM dynamic import inside the VFS, single cordis instance, fail-loud config chain, `node:sqlite`, macOS ad-hoc signing runs). SDK tier: the complete keyless pytest suite covers the client protocol against a fake runtime peer, subprocess cleanup, absolute cwd propagation, dual-carrier launch, and carrier resolution; root CI runs it on Python 3.10. End-to-end tier: every platform build installs both wheels into a clean venv outside the checkout, proves matching versions and installed module/executable locations, then completes turns against a mock endpoint through the default SDK path, a custom config, the checked-in standalone minimal composition, and the direct binary protocol, with final text and JSONL checked. The minimal run asserts its exact system prompt and single shell-tool catalog and retains shell state across calls. The custom config additionally drives `run_code` and a zero-agent `workflow` through their real worker files inside the packaged VFS. The filesystem-search scenario requires the model to call both `glob` and `grep` through the target-native `-rg` sidecar. The spawn-node scenario drives the platform shell tool through a command starting with `node` and requires the machine's own Node version in the tool result with no `PKG_EXECPATH` in the child environment, pinning the packaged runtime against a pkg upgrade that re-records the child-process patch. The MCP scenario starts a temporary external stdio server, deliberately delays its initial `tools/list` response, then immediately starts the first SDK prompt; the prompt must see and call the discovered tool, proving that `initialize` is a real Loader-settlement readiness boundary rather than a timing sleep. The same installed run compares a committed executable-specific snapshot through the Python SDK: a keyless scripted model mounts a Cordis plugin that registers a tool, invokes that tool from `run_code`, runs a direct spawn subagent and a workflow that starts a second spawn child, then unmounts the plugin. The fixture explicitly disables its unused bundled Bash and local skill discovery so its tool set does not depend on repository-external state, and the comparison normalizes opaque message, agent, workflow-run, and session IDs across the SDK result and notification stream plus the parent and two child JSONL logs. Trusted pull requests add a real-provider two-turn file write/read whose external bytes, tool calls, completed reasons, and persisted log must agree. This harness stays separate from ACP's `pnpm run test:snapshot` because the protocols and build artifacts differ.
The verification surface has three tiers. Mechanism tier: the measured conclusions for the `--sea` chain are embedded in the Decision sections (ESM dynamic import inside the VFS, single cordis instance, fail-loud config chain, `node:sqlite`, macOS ad-hoc signing runs). SDK tier: the complete keyless pytest suite covers the client protocol against a fake runtime peer, subprocess cleanup, absolute cwd propagation, dual-carrier launch, and carrier resolution; root CI runs it on Python 3.10. End-to-end tier: every platform build installs both wheels into a clean venv outside the checkout, proves matching versions and installed module/executable locations, then completes turns against a mock endpoint through the default SDK path, a custom config, the checked-in standalone minimal composition, and the direct binary protocol, with final text and JSONL checked. The minimal run asserts its exact system prompt and single shell-tool catalog and retains shell state across calls. The custom config additionally drives `run_code` and a zero-agent `workflow` through the shared packaged Node process bootstrap. The filesystem-search scenario requires the model to call both `glob` and `grep` through the target-native `-rg` sidecar. The spawn-node scenario drives the platform shell tool through a command starting with `node` and requires the machine's own Node version in the tool result with no `PKG_EXECPATH` in the child environment, pinning the packaged runtime against a pkg upgrade that re-records the child-process patch. The MCP scenario starts a temporary external stdio server, deliberately delays its initial `tools/list` response, then immediately starts the first SDK prompt; the prompt must see and call the discovered tool, proving that `initialize` is a real Loader-settlement readiness boundary rather than a timing sleep. The same installed run compares a committed executable-specific snapshot through the Python SDK: a keyless scripted model mounts a Cordis plugin that registers a tool, invokes that tool from `run_code`, runs a direct spawn subagent and a workflow that starts a second spawn child, then unmounts the plugin. The fixture explicitly disables its unused bundled Bash and local skill discovery so its tool set does not depend on repository-external state, and the comparison normalizes opaque message, agent, workflow-run, and session IDs across the SDK result and notification stream plus the parent and two child JSONL logs. Trusted pull requests add a real-provider two-turn file write/read whose external bytes, tool calls, completed reasons, and persisted log must agree. This harness stays separate from ACP's `pnpm run test:snapshot` because the protocols and build artifacts differ.
Manual-driving caveat: the bin treats stdin EOF as "the client is gone" and disposes immediately, so a short-lived pipe aborts an in-flight turn — pipe-driven runs must keep stdin open until the turn ends.
The installed-wheel `sdk-office` scenario copies the target's complete payload to a private temporary directory, requires exactly one target backend (the declared native target, or WASM when no native target is declared), and converts DOCX once through the shipped SDK profile with that engine. It checks that the kit module resolves inside the relocated directory, that conversion reports the expected backend, and that the output contains PDF bytes. Wheel validation checks the declared engine assets and native helper execute bit; CI uploads the completed wheel, so nested helper permissions stay inside the ZIP archive.
## Alternatives considered
**Bare Node native SEA.** The injected main script must be a single CJS file, and the blob carries no filesystem and no module resolution, so a dynamic import of a bare specifier has nothing to resolve against; the only option is compiling plugins statically into the main script and registering them by hand — bypassing standard module resolution and hardcoding the plugin set, contrary to "configuration decides everything". The final route is in fact "the official SEA foundation + pkg's VFS/module-hook layer"; what was rejected is the bare use, not SEA itself.
@@ -84,6 +90,6 @@ Manual-driving caveat: the bin treats stdin EOF as "the client is gone" and disp
## Consequences
**Bought**: zero-dependency single-file distribution on target platforms; plugin semantics strictly identical to running from source (the same real package tree, no transpilation, no registry); the serving interface, the plugin set, and the configuration all converge on two sources of truth — `cordis.yml` plus one dependency manifest; the exe and node carriers share one tree and one semantics, so development verification never waits for packaging; official Node binaries remove the patched-binary supply-chain concern.
**Bought**: no system Node.js dependency on supported targets; plugin semantics strictly identical to running from source (the same real package tree, no transpilation, no registry); the serving interface, the plugin set, and the configuration all converge on two sources of truth — `cordis.yml` plus one dependency manifest; the exe and node carriers share one tree and one semantics, so development verification never waits for packaging; official Node binaries remove the patched-binary supply-chain concern.
**Paid**: artifacts on the order of 174MB with source entering the blob as-is (no bytecode obfuscation; a closed-source distribution requirement needs a separate evaluation); pkg's VFS/module-hook layer remains community-maintained (`@yao-pkg/pkg` is an exact-pinned, pnpm-patched root devDependency; upgrading re-records the patch and is an explicit change); `--sea` is one invocation per target (matching CI's one leg per platform; local multi-platform builds are serial).
**Paid**: large Office engine resources remain external to the SEA archive, and the executable must travel with its required sidecars; source enters the distribution as-is (no bytecode obfuscation; a closed-source distribution requirement needs a separate evaluation); pkg's VFS/module-hook layer remains community-maintained (`@yao-pkg/pkg` is an exact-pinned, pnpm-patched root devDependency; upgrading re-records the patch and is an explicit change); `--sea` is one invocation per target (matching CI's one leg per platform; local multi-platform builds are serial).
@@ -60,6 +60,8 @@ Product helpers therefore construct the carrier and pass the domain subject sepa
A Cordis waterfall is middleware-style dispatch. Each listener receives `next()`: calling it delegates to the remaining listeners and base operation, while returning without it short-circuits or replaces the downstream result. Waterfalls power prompt assembly and tool policy; ordinary emit events notify synchronously, and parallel events await all listeners without a veto result.
## Scope routing: one opaque key selects one layer
The scope package implements the smallest object needed for Cordis routing. Its carrier holds only a composed service filter and scope predicate, while the package records the opaque key privately and exposes the scope fiber's quiescent disposer separately.
@@ -117,30 +119,28 @@ Publication admits and announces resources in the order required by observers:
1. Enter the session.
2. Enter the agent.
3. Announce `session/created`.
4. Announce`agent/created`.
5. Enable public driving.
6. Emit `agent/session-start`.
7. Start the driver.
4. Await serial`agent/created` listeners.
5. Release queued input to the driver.
The agent never drives before both registries and creation notifications agree. A synchronous listener may veto or dispose an owner; the transaction records publication in progress and waits for that callback stack to unwind before teardown continues. Every creation announcement that begins has a matching disposal announcement during rollback.
The agent never drives before both registries and creation listeners finish. A listener may reject or dispose an owner; the transaction retains the scope and session until dispatch settles before teardown continues. Every creation announcement that begins has a matching disposal announcement during rollback. The [awaited creation decision](2026-09-09-awaited-agent-creation.md) owns asynchronous initializer timing.
The sequence diagram isolates the non-obvious race: a synchronous creation listener can request disposal while the publication call stack still owns both registry entries. Teardown must deactivate immediately but wait for that stack to unwind before stopping and detaching anything.
A creation listener can request disposal while publication still owns both registry entries. Teardown deactivates immediately and waits for the awaited dispatch before stopping and detaching anything.
```mermaid
sequenceDiagram
participant Tx as AgentCreationTransaction
participant Registries
participant Listener as Synchronous listener
participant Listener as Creation listener
participant Driver
Tx->>Tx: mark publication in progress
Tx->>Registries: announce agent/created
Registries->>Listener: invoke inside the same call stack
Registries->>Listener: await listener
Listener->>Tx: dispose reentrantly
Tx->>Tx: deactivate, teardown waits for publication
Tx-->>Listener: disposal request accepted
Listener-->>Registries: return
Registries-->>Tx: announcement unwound
Listener-->>Registries: settle
Registries-->>Tx: dispatch settled
Tx->>Tx: resolve publication settlement
Tx->>Driver: stop and drain
Tx->>Registries: detach agent, then session
@@ -151,7 +151,7 @@ sequenceDiagram
Every teardown request joins one memoized path. The order is:
1. Deactivate creation or driving and let synchronous publication finish.
1. Deactivate creation or driving and await creation dispatch.
2. Stop and drain the driver, discarding any injection that remains pending.
3. Detach the agent.
4. Detach the session.
@@ -160,6 +160,8 @@ Every teardown request joins one memoized path. The order is:
This order lets final agent and session events use the matching scoped listeners and keeps persistence observers attached through the final flush. Scope disposal comes last because registration revocation is the externally visible lifetime boundary.
Session events cross a durable boundary, so append owns their data. The rest of the algorithm uses one attached entry and one commit point.
@@ -232,6 +234,8 @@ This is a trusted same-process extension point, not an authority boundary. A lis
Scope solves the real isolation problem directly. Structured-output contributions register in the child's exact scope, while PTC mode derives its transport and SDK from the same resolved tool view. A second named-protection system would need another ownership and collision rule across arbitrary schema providers—including providers that intentionally contribute duplicate names—without creating a new trust boundary.
### Structured output commits only authoritative outcomes
Structured output combines child-scoped composition with a two-phase execution commit. The child registers its `structured_output` tool and instruction before publication; a trusted assembly listener may transform those ordinary contributions and is responsible for preserving the protocol if the child is expected to complete. The tool body validates a candidate and stages it by the current `ToolExecution`, but successful capture is decided only by immutable `tools/result` observations.
@@ -244,6 +248,8 @@ Once a value is pending or committed, a scoped monotonic guard denies later tool
Pure PTC mode's registry contribution omits `structured_output` from native wire schemas and exposes it through the generated SDK. The assembly waterfall may deliberately change that presentation; execution still validates against the child-scoped definition, and the listener owns the consistency of any alternate model-visible route it creates.
### Three execution boundaries are deliberately one-way
Prompt assembly is intentionally cooperative, but three execution facts need one-way settlement after their extensible stages:
@@ -294,19 +300,21 @@ Start resolves only after `initialize` and `newSession` succeed. Abort, spawn fa
Worker and child-process bridges need more state than same-process registries because messages, process death, and cleanup can settle independently. Their state is organized around those real facts rather than duplicate cancellation protocols.
### Workflow children are pending starts or published records
The workflow host keeps pending provider-start promises and published child records. A child moves from pending to published only when async `SubagentRuntime.start()` fulfills; rejected starts clean their partial provider work and produce no child lifecycle pair.
One host-owned AbortController supplies the required signal to pending and live children. Closing workflow admission aborts that signal, so there is no duplicate `ChildCancel` worker RPC or explicit host-side `run.cancel()` fanout. Quiescence waits for both pending starts and published child disposal.
One host-owned AbortController supplies the required signal to pending and live children. Closing workflow admission aborts that signal; quiescence waits for both pending starts and published child disposal. [Workflow sandbox reuse](2026-09-13-workflow-ptc-sandbox-reuse.md) owns PTC process cancellation and the absence of a separate workflow cleanup timer.
The worker boundary still serializes requests and outcomes. The host retains first-terminal-outcome arbitration, exact child accounting, worker-death handling, grace termination, late/duplicate message rejection, and bounded cleanup because result receipt, worker exit, and child quiescence are genuinely independent facts.
PTC serializes requests and outcomes and owns process termination. The workflow adapter retains terminal-outcome arbitration and child ownership because program settlement, process exit and child quiescence remain independent facts.
### Terminal result and physical cleanup remain separate
The workflow result records the first accepted terminal outcome according to the public precedence rules. Cleanup can continue after that result is chosen: live children still need disposal, a worker still needs termination, and a slow external backend may outlive the configured grace bound.
The workflow result records the first accepted terminal outcome according to the public precedence rules. Choosing that outcome does not release resources: the PTC process and live children still need cleanup, and child disposal must fulfill its provider contract.
Public disposal claims its memoized promise before invoking callbacks. Worker death closes admission before processing any queued late child request, synthesizes missing lifecycle ends, and starts child/process cleanup without rewriting an outcome already claimed.
Public disposal joins one cleanup operation. Run settlement closes child admission, synthesizes missing lifecycle ends and cleans up children without rewriting an outcome already claimed.
### ACP prompt settlement does not depend on update delivery
@@ -334,7 +342,7 @@ The plugin does not police trusted setup by scanning registries or reject prompt
The event catalog, service catalog, producer/consumer matrix, configuration catalog, module graph, tool catalog, type-equivalence blocks, and scoped-event resolver map are generated or freshness-gated from source. The [TypeScript semantic-gates Agent Note](../../archived/process/2026-07-14-typescript-program-backed-semantic-gates.md) owns Program construction, semantic event discovery, and resolver-generation rules.
Behavioral tests pin scoped routing and disposal, final-entry collision cleanup, publication rollback, ordered quiescence, durable pre/post-commit behavior, live tool filtering across presentation and execution, cooperative prompt assembly, structured-output commit in native and PTC mode, async subagent startup and signal cancellation, worker terminal arbitration, ACP settlement, and process teardown.
Behavioral tests pin scoped routing and disposal, final-entry collision cleanup, publication rollback, ordered quiescence, durable pre/post-commit behavior, live tool filtering across presentation and execution, cooperative prompt assembly, structured-output commit in native and PTC mode, async subagent startup and signal cancellation, workflow terminal arbitration, ACP settlement, and process teardown.
@@ -12,7 +12,7 @@ The [initiating Agent scope decision](2026-07-15-agent-initiator-scope.md) inten
## Decision
Agent owns the runtime-only `AgentCancelCause` union `{ kind: 'user' } | { kind: 'parent' }`; `agent.cancel()` defaults to `user`. TypeScript enforces that vocabulary at this typed same-process boundary, with no runtime validator, fallback, or special compatibility contract for untyped callers. An active `TurnCancellation` copies the typed discriminant into a fresh frozen signal reason; idle cancellation has no holder to mutate and does not arm later work.
Agent owns the runtime-only `AgentCancelCause` union `{ kind: 'user' } | { kind: 'parent' } | { kind: 'hook'; reason: string } | { kind: 'disposed' }`, and `agent.cancel()` requires one. TypeScript enforces that vocabulary at this typed same-process boundary, with no runtime validator, fallback, or special compatibility contract for untyped callers. The holder aborts with the caller's own object rather than a detached copy, so a transport reaching that signal can extend it: Node's fetch assigns a `stack` onto the reason it receives. The loop therefore copies the declared fields where the cause becomes durable data. Freezing the cause at `cancel()` is not an alternative — undici performs that assignment from strict-mode code, so a frozen reason makes `fetch` reject with `TypeError: Cannot add property stack` in place of the abort reason. Idle cancellation has no holder to mutate and does not arm later work.
An interrupted live turn ends with the coarse durable `{ kind: 'aborted' }` outcome. The terminal event records what happened to the turn, while the runtime signal identifies who requested cancellation; it does not duplicate `user` or `parent` into replay. Session seed/load rejects legacy aborted records with a reason or any other extra field, so replay cannot reintroduce caller-owned cancellation detail. The process-local `agent/cancel-requested` notification is not durable; a future audit requirement uses a separate durable control-request event so a request and its eventual outcome remain distinct. Durable events contain no stack, signal, error object, free-form cancellation text, or backend-private detail.
@@ -22,7 +22,7 @@ The driver keeps only a cause-less pre-run marker for queued work cancelled befo
The explicit event signatures pass a single payload object: agent-scoped events carry `agent` and `signal` in the payload with `next` last, and the remaining APIs keep `signal` immediately before a waterfall's final `next`. `PreStepContext` and `RequestFailureContext` are retired, with their fields folded into the `agent/pre-step` and `agent/request-error` payloads ([payload-object events](../../archived/architecture/2026-08-06-agent-event-payload-objects.md)). Pre-step entry, request configuration, request-error recovery, model generation, tool execution, approval, turn stopping, and subagent or workflow requests all receive the current signal. Hook bridges must also supply `RunHookOptions.signal`, so a turn cancellation reaches the bash executor's provider-managed termination and join boundary; the [native-containment decision](2026-08-28-subprocess-native-containment.md) owns the supported local scope, Job, and fallback mechanics. `SystemPrompt.assemble()` carries `signal?: AbortSignal` in `AssembleContext` because that object is an explicit request value that can also represent signal-less assembly outside a turn. Listeners may cooperate with the signal but must not retain it to control another turn.
`ctx.agents` continues to carry only the initiating Agent. Ambient Agent presence does not imply liveness, a current turn, or cancellation authority. The cause reader is private to the loop and states the machine-private slot invariant (only `cancel()` aborts a turn controller, always with a canonical frozen cause) instead of re-validating the reason structurally; no public helper reads a cause off an arbitrary signal. Concurrent Agents isolate both their initiator identities and their turn signals; a child driver shadows the parent initiator while its parent request signal still travels through the subagent seam.
`ctx.agents` continues to carry only the initiating Agent. Ambient Agent presence does not imply liveness, a current turn, or cancellation authority. The cause reader is private to the loop and states the machine-private slot invariant (only `cancel()` aborts a turn controller, always with a typed cause) instead of re-validating the reason structurally; no public helper reads a cause off an arbitrary signal. Concurrent Agents isolate both their initiator identities and their turn signals; a child driver shadows the parent initiator while its parent request signal still travels through the subagent seam.
Agent disposal requests the runtime-only `{ kind: 'disposed' }` interruption on the active holder. If cancellation already won the controller reason, the reason cannot be rewritten, so terminal classification first checks lifecycle state: disposed wins, then a supported `user` or `parent` cause becomes the coarse aborted outcome, and unrelated exceptions retain the existing error path. ACP cancellation maps to `user`; in-process spawn and fork propagation map to `parent`. Remote ACP subagents retain their existing wire protocol.
@@ -30,7 +30,7 @@ Cancellation remains cooperative. The loop checks interruption before and after
## Verification
Contract tests verify the typed caller union, frozen detachment, default and first-wins behavior, the coarse Session JSON round trip and legacy-record rejection, ACP `user`, in-process subagent `parent`, and disposal precedence. Loop tests make cooperative listeners wait on the signal at pre-step, system-prompt assembly, request, model stream, request-error recovery, tool execution, and turn stopping; they assert one signal within a turn, a fresh signal across turns, and no cancellation authority during terminal publication or a blocked durability flush. A real hook bridge test cancels and reaps a blocked prompt hook before idle.
Contract tests verify the typed caller union, field copying where the cause becomes durable data, default and first-wins behavior, the coarse Session JSON round trip and legacy-record rejection, ACP `user`, in-process subagent `parent`, and disposal precedence. Loop tests make cooperative listeners wait on the signal at pre-step, system-prompt assembly, request, model stream, request-error recovery, tool execution, and turn stopping; they assert one signal within a turn, a fresh signal across turns, and no cancellation authority during terminal publication or a blocked durability flush. A real hook bridge test cancels and reaps a blocked prompt hook before idle.
Initiator-scope tests assert that every hook still observes the exact Agent and no ambient turn signal, concurrent Agents retain independent identities and signals, and a nested child driver shadows only identity. Race tests cover idle cancellation, pre-run cancellation, replacement submission from a `running` listener, repeated cancellation, and cancel-versus-dispose quiescence.
Loaded 100 of 9138 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.