- 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.
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.
* 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.
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 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 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.
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 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.
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 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
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.
* 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.
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.
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.
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.
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.
The label-dimmed alias rides the bluish neutral ramp, whose cast shows
at the 56px compass; the hero now takes neutral-200 (light) and
neutral-700 (dark) from the pure-gray static ramp.
The guide tab gains a compass hero, per-entry descriptions (shown while
at most four entries are listed), and a quieter cube placeholder for
entries that registered no glyph; the files entry registers its
description and folder glyph. Bundled stat fixes: the session usage
dialog drops the zero cache-write row, and the turn-time pill renders as
plain text when its dialog would only repeat the pill's own duration.
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.
Seed sidebar default pages lazily on expansion, scope page-tab dedupe
to the target pane, let a sole tab split its own pane with a factory
backfill, close menus when focus enters a sandboxed iframe, and pin the
code preview copy banner while unifying its background with the pane.
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.
A file whose stat and first page both fail — outside the workspace, gone —
showed the same line twice: the metadata bar above an identical centred body
failure. The bar now stands only over pages already loaded, and the centred
retry runs the full reload so one press clears both halves.
The expand/collapse and wrap toggles' accessible names diverged from their
tooltips, and the replay harness now normalizes clientTimeZone in recorded
sessions; every golden holding the old names or the literal zone refreshes.
- Split toggle aria-labels from their tooltips: the sidebar expand/collapse
buttons announce "Open/Collapse right sidebar" so the two sidebars stay
distinguishable, and the wrap toggle keeps an invariant "Line wrap" name
beside aria-pressed while the tooltip keeps the action wording.
- A hidden tab close also takes no pointer events, so a touch press on the
chip's trailing edge activates the tab instead of closing it; e2e closes
hover the chip first, the way a reader would.
- Closing the last docked tab in fullscreen hands the mode back to push, so
the next expand does not give the whole window to the start page.
- The drop scrim mixes from bg-base, not bg-layer-2, which dark mode binds
to a lighter bluish step than the column beneath it.
- Document the menuitem probe in TabMenuExtras and the strip fade's known
gap; match the open-in-app split-button comment to the 28px control row.
The lone guide tab withholds its close and menu, any other lone tab closes
together with the column, and the expand button renders only while collapsed
so the corner seat costs the header nothing.
Chip capsules with scroll fades and live titles, drop-zone hint cards, the
canCloseTab control policy with the quiet lone chip and self-dismissing empty
menu, and a room reading that stays independent of hidden split controls.
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.
Dislike and a bare /feedback open the Session's feedback dialog, which
collects one of seven fixed categories and an optional description and
records through the messageFeedback put or the new sessionFeedback
Remote; Like records at once. A recorded rating shows the filled glyph
and every submission is acknowledged by a toast. feedback/record and
MessageFeedbackItem carry the optional category; the note popover, its
layout scenario, and the clearNote/clear verbs are removed. ui-commands
gains the action command kind the /feedback decoration runs as.
Closes#3515
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.
Merge 3ea52fc3e475a3e7c430812bde4209258416e57b into a0a61a8237f61344a01448a97954df603d8fff73. Retain the V2-to-V3 migration dependency alongside native system flock support, removing the replaced fs-ext dependency. Union the lockfile workspace importer entries; frozen installation validates the result without regeneration.
Preserve release publint async lane-signal cancellation, close ownership, and spawn-error regressions, satisfying the incoming CI budget intent. Keep canonical, prompt, preset, and latest migration regression behavior. Rename an arbitrary preset fixture value to custom-agent to avoid the inherited vendor-rescope false positive without changing test semantics.
Validation: frozen install; full build including Darwin addon; focused V3/persistence/prompt/publint and runtime tests; built migration, two-process lease, and loader smokes; native flock and package-matrix tests; touched dependency and CI owner tests; all hygiene leaves (vendor residue repaired and rechecked); doc-sync 34/34.
The minimal compositions serve the same models that trained against a
one-shot `bash` tool; three surface differences remained visible to them.
- Every settled command now appends `[Command finished with exit code N]`,
including exit code 0, instead of `[exit code: N]` only on failure. A
timeout appends `[Command timed out or OOM]` after the partial output.
- Trailing newlines are all dropped before the status trailer rather than
only the last one.
- The `bash` description in the `minimal` preset and the `sdk-minimal`
bundle replaces the two fixed network lines with a single environment-
dependent line; the pwsh description is unchanged.
Tests, READMEs, the web minimal-preset schema snapshot, and the packaged
Python minimal model-visible snapshot follow the new wording. The
standalone `persistent-tools` SDK replay fixture keeps its own copy of the
old description since it is a per-launch customization, not the profile.
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.
The icon-set spec still pinned 74 exports after IconGaugeOutline16
landed (a hand-drawn product glyph — the set is now eight of those).
Moving the preview badge inside the hero title group had merged the
headline into one text node with the badge, which broke exact text
lookup (hmr-live e2e); the headline gets its own span again.
StatsPills buttons carry explicit aria-labels separating segments with
' · '; a log with no timed figure renders the counts pill as a static
reading instead of a button opening an empty dialog; the pills row owns
one exclusive open slot so sibling dialogs never stack. ContextMeter
restores the '~' approximation prefix (usedTokens prefers heuristic
projectedTokens). ModelSelect's jscpd:ignore now names the real gap
(useAnchoredPosition places from the left edge only). The pills Agent
Note's stale facts (locale keys, context occupancy, golden ownership)
are rewritten in place, en+zh. plan-review.e2e.ts parks the pointer
after Approve so the ContextMeter tooltip cannot leak into the aria
captures; affected goldens refreshed.
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.
The plain-to-capable transition emits the model-selection notice inherited from master. Preserve the exact in-history system ordering and assert both the admitted user input and the single durable notice instead of expecting the obsolete five-message request. Reproduces the identical Linux and Windows CI failure; all 57 adjacent admission, projection, reconstruction and selection tests pass.
Master archived three Agent Notes this branch had fact-updated (archived
copies restored to their frozen master content; the pills note now links
the archived path), extended the steer-all placeholder to continuable
children (spec and subagent e2e take master's behavior with this branch's
reworded copy), and dropped the session-sharing sentence from feedback
acks (goldens re-refreshed with the new placeholder).
The 2026-08-06 continuable-subagent interrupt note said the child's Send
always queued; it now names the busy-Enter delivery and cross-links the
busy Send button note, which links back. The new note's and bench's
plain-Send enumerations now exclude the ordinary running empty/blocked
draft, whose seat is Stop.
The A/B winner over the single-line StatsLine: a gauge pill (counts + TPS,
opening the session-stats dialog) and a database pill (billed total +
cache-hit share, opening the exact token-usage dialog), sharing a new
stat-dialog module. StatsLine and its dead locale keys are deleted, the
gauge icon is optically centered, and the composer keeps its 4px clearance
through the data-composer-stats contract. Agent Note added; stale
StatsLine references across implemented notes updated in place.
Review follow-up: the mode label predicate now includes !uploadsPending,
so a running draft whose file is still uploading (or failed) keeps the
plain Send label on its disabled button, matching the documented
'enabled button' condition. Test pins pending vs ready upload states;
the Agent Note and README pair name the upload gate.
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.
Include the root merge from #3678 without altering the previously validated implementation tree. Preserve structural V3 system messages and the current native-runtime recordings.
Review follow-up. The mode label predicate lacked `!empty`, so a running
continuable child with an empty draft showed a disabled "Queue message";
and a claimed slash command or a `/` line headed for adjudication executes
a command rather than delivering a message, so naming Queue/Steer there
misdescribed the click. The label now names the mode only when the click
would deliver a plain message: running, steer-capable, enabled, non-empty,
unclaimed, and not a `/` line. Tests pin the empty-draft child, the slash
line, and the claimed command; the Agent Note, README pair, and
ComposerKeyboard.submit JSDoc state the exact predicate.
`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 ui-conversation.busyEnter setting selected Queue or Steer for plain
Enter only; the running draft's Send button always submitted through the
public InputActions.submit() face, which is fixed to Queue, while labeled
"Send message". A user who chose Steer got Steer from Enter and Queue
from the button beside the same draft with nothing explaining why.
InputBar now resolves the primary click through the same `enter` gesture
and steer-availability predicate as the keyboard path (ordinary Sessions
and continuable children) and submits via ComposerKeyboard.submit(mode).
While that delivery is available the button's tooltip and accessible name
state it ("Queue message" / "Steer message"; 排队发送 / 插话发送), so the
mode is never hidden; idle sessions, one-shot children, and locked
composers keep the plain Send label. The composer bar inject face
publishes the live preference as hooks.busyEnter instead of a resolver
closure, and resolveSubmitMode is a pure function taking the preference
explicitly, so label and delivery derive from one value in one render and
the label follows live Settings changes. The Settings row is retitled
"Send behavior while busy" with a description that names both Enter and
the button; the busyEnter field, its default, and the Host schema are
unchanged.
Tests pin both preferences on the running button for ordinary sessions
and continuable children, the live relabel, the idle Queue path, and the
pure resolver; the settings-chrome ARIA goldens and the live-interactions
running-draft and queue-actions failed goldens carry the new copy. A new
Agent Note owns the decision and reverses the pointer-ignores-preference
clause of the archived 2026-08-20 running-draft note.
Preserve both sdk-minimal-in-history and native runner smoke scenarios. Record the current Cordis inspection through the actual upper headless owner while retaining upstream V2 bytes and strict prompt admission semantics.
Preserve strict live-smoke challenges and structural system messages while adopting the native subprocess runtime. Regenerate only the current Cordis inspection recording from the upstream V2 source to retain its no-ordinary-PID coverage; predecessor bytes match master.
Preserve the root V3 current-writer, PTC dispatch identity, and reliability fixes while incorporating provider-owned native ranges and the private packaged runner bootstrap.
Refresh only the current cordis-inspect-jsdoc V3 owner through the built headless profile so upstream subprocess API coverage is exercised. Preserve incoming master V2 bytes and all other fixtures and benchmark thresholds.
Preserve prepared-route admission and V3 prompt semantics while adopting latest master. Regenerate the new model-switch recording at its actual upper-layer title ordering, retain the V2 predecessor, and keep queued-image and publint lifetime assertions strict.
Adopt current model-switch notices, lazy observation, and open-in-app integration while preserving structural V3 admission. Regenerate only the new model-switch owner’s native V3 recording, prompt-count manifest and expected delimiter; keep the master V2 predecessor byte-exact.
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.
Bring model-switch notices, lazy observations, and open-in-app into the root V3 integration without importing the child structural header changes. Preserve V3 migration, PTC, and all predecessor fixtures.
Refresh only the model-switch-notice owner through dsh --profile headless to harvest native current-writer session.v3.jsonl. Keep the master session.v2.jsonl byte-exact; existing header pin and versioned writer helpers require no changes.
- A media-named FIFO or device node is refused before the open would
block on it (pre-open isFile check); POSIX FIFO regression test added.
- The full-body stream is bounded to the stat'ed size so concurrent
appends cannot exceed the declared Content-Length.
- Error responses carry no body for HEAD requests.
- Docs no longer overclaim the replacement race: the stat-identity
comparison narrows (does not fully close) the replacement window, and
the earlier realpath-to-stat window is acknowledged. Pairs re-recorded.
range-parser never validates the range unit, so /api/file now parses only
headers that start with the bytes unit: unsatisfiable ranges answer 416,
while malformed, unknown-unit, and multi-range headers are ignored for a
full 200 body per RFC 9110. The filesystem-root workspace case stays
POSIX-only in tests (a Windows drive-root spelling cannot be constructed
portably); containment already treats any separator-terminated root the
same. Docs (README pair, Agent Note pair) re-recorded.
Review follow-up. A forwarding module's explicit re-exports are tried
before its star re-exports and an entered module is skipped on later
edges, so a star edge that loops back no longer hides the explicit
re-export beside it. Import resolution outside the checker goes through
the face's shared compiler host and module-resolution cache, and an
import naming an unregistered package fails the public-export check
instead of throwing a TypeError. Tests pin the looping star, the
relative-only cycle exit, namespace re-exports, and re-exported
namespace imports.
Add the first-party open-in-app host and client packages with a localized Open In menu in the Web Session header.
Resolve installed applications on macOS, Windows, and Linux, launch workspace directories through platform-specific adapters, and remember the selected application.
Closes#1500.
Co-authored-by: ihsiang <[email protected]>
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.
- HEAD never opens a file stream; GET streams are destroyed on client
abort and are never opened for already-aborted requests.
- Validation and reading bind to the same opened file: pre-open and
opened stat identities are compared, so replacement/re-linking races
are refused (dev/ino check, read through the FileHandle).
- Workspace containment compares path components, so a filesystem-root
workspace serves files instead of doubling the separator.
- Multi-range headers are ignored for a full 200 body instead of a
mislabeled single-segment 206; single-range behavior unchanged.
- Tests now cover the reviewed edges (empty path, multi-range, root
workspace, HEAD+range, mid/early abort, unreadable file, explicit
unregister-on-dispose, private outside-target directories); module
coverage is back at 100/100/100/100.
- Agent Note bilingual facts and the ui-primitives/session-controller
README pairs are synced to the shipped behavior (pairs re-recorded).
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.
Include the root merge ancestry from #3669. Preserve the already validated system-message child recording rather than restoring the root’s header-system representation. The resulting tree is unchanged from the preceding representation head.
A single-consumer pure helper does not earn its own file: the mapping
now lives beside the prose vocabulary it serves (same shape as the
deliverables file-mention logic), and its unit cases merged into the
component spec.
- /api/file module now exports only the SessionMediaReferences plugin
contribution; MIME and range policy helpers are module-private and
exercised entirely through the registered route.
- Replace the hand-rolled media extension table with mime-types (served
categories image/video/audio, excluding image/svg+xml) and the
hand-rolled Range parser with range-parser; keep the fail-closed
workspace containment policy.
- Spec rewritten as route-level behavior tests (12 cases) covering the
same branches; Agent Note facts updated in the same change.
`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.
Observe actual shared-preparation waiter admission and settle both callers before cleanup, preserving current V3 migration expectations. Adopt master’s cross-platform teardown regressions without changing budgets or Session data.
Observe actual shared-preparation waiter admission and settle both callers before cleanup, preserving current V3 migration expectations. Adopt master’s cross-platform teardown regressions without changing budgets or Session data.
A type imported by relative path from a module of the same package that
re-exports it from another package failed as a cross-package reference
without an explicit package import. The analyzer now resolves the relative
specifier, follows re-exports while the file stays inside the referencing
package, and applies the public-export check to the package specifier the
chain reaches. Relative paths into other packages and forwarded private
exports still fail.
Fixes#3525
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.
Drop the byte-signature sniff duplicated from fs/tool-fs's read_image tool:
the cross-file duplication gate forbids the clone, and no shared owner
exists without widening the attachment package's public API. The
extension allowlist keeps non-media content out; corrupt image payloads
fail in the browser, not on the route. Agent Note facts updated in the
same change.
Preserve in-history request ordering and native V3 system heads while incorporating master’s human inbox controls. Regenerate only the current SDK child recording through its actual owner; retained V2 predecessor bytes match master.
Record current V3 output from master’s human inbox control scenario while retaining the V2 predecessor unchanged. No additional product behavior or validation change.
Refresh the current V3 child recording through the actual SDK scenario after master’s inbox-control change. Preserve the V2 predecessor inherited from master and retain system-message admission and current references.
Preserve the V3 PTC SDK assertion and enable the new human-steering runtime fixture while carrying master’s inbox lifecycle changes. Retain prior deterministic CI repairs and immutable predecessor ownership.
Preserve the V3 PTC scenario while enabling the current subagent human-steering fixture environment. All product changes are inherited from master; no V3 migration semantics are altered.
* feat(subagent): align continuable human inbox controls
* fix: repair rebase documentation and close test persistence
* docs: refresh rebased module dependency graph
Assistant prose that references a workspace-contained local media path
(e.g. ``) now renders through a same-origin
`GET|HEAD /api/file?path=` route instead of inert alt text.
- ui-primitives: MarkdownText gains a settled-only MarkdownPathImages
vocabulary gate (same posture as file mentions); no vocabulary means
byte-identical output.
- ui-chat: AssistantMarkdown supplies a page-stable rewrite vocabulary
for absolute POSIX paths (local-path-media.ts).
- session-controller: SessionMediaReferences plugin contribution mounts
the route on the authenticated connection.fetch channel; per-request
policy = workspace-root containment after realpath, regular file,
allowlisted media extension (images additionally signature-checked),
range/HEAD streaming, private no-store + nosniff, fail-closed statuses.
- Agent Note added (feature/2026-09-07-session-prose-local-media-display).
Closes#3662.
Preserve the V3 system-prompt projection while adopting master’s per-agent frozen-message provenance and frontend performance gates. Keep benchmark workload admission and diagnostics fixes intact, and retain both migration and first-open/reopen lifecycle guarantees.
Generated V0 logs put user surfaces before the first step and cannot migrate to a protected V3 system head without changing chronology. Author native current headers, open the first step before the empty system head and user, keep title sequence references aligned, and supply embedded assistant streams.
Give the preset child a current identified user message. Preserve all paging, geometry, preset assertions and committed historical fixtures. Validated both generators red/green through the source seed reader, one full build, all preset/stats/trajectory browser tests (11), and scoped type-aware lint.
The long-chat builder emitted current V3 with user/message before system/message, so seed restoration rejected its protected head before browser assertions ran. Open the first step and append the system head before the user surface; keep Session-assigned title and tool-result references.
Add an independent seed-reader regression for current version, protected head order, title/tool references and closed turns. Observed protected-head failure before the fix; focused regression and full configured type-aware lint pass. Existing browser scroll assertions are unchanged; full built owner runs follow.
Adopt master’s explicit personaPrefix/personaSuffix configuration in the in-history test harness. The assembly override and all admission assertions remain unchanged; this fixes the typed build integration rather than weakening the tests.
Keep prepared-route in-history append, clear-all, and normalization semantics while adopting named persona prefix/suffix and late environment facts from master. Resolve documentation against the derived system-message history and retain the existing strict migration behavior.
Preserve system/message persistence while adopting named persona prefix/suffix and environment facts after reusable instructions. Resolve the Web assertion against the logged system message, not retired header.system. Keep the existing protected V3 integration PR as the root publication path; no rules are bypassed.
Preserve valid native V3 system heads in resumed fixture seeds and inherit the scanner coverage repair. Regenerate only upper-layer owner-local expected output through the actual loop so fallback title follows header/context. All five affected expected cases pass refresh; no historical Session generation or assertion is weakened.
Keep the synthetic compressed historical fixture step-first without changing frozen generation data or shared fixtures. Pin its migrated empty system head and unchanged source bytes. Exercise malformed native system rows before and after recoverable damage through scanner and read/write handles. Remove the unreachable duplicate unsupported-format catch after current-row admission; preserve its owned rejection and generic error path.
Validation: original zstd regression red; admission-bypass negative control 3/3 red; five focused specs 314/314 pass with format.ts 100% statements/branches/functions/lines. Final admission cleanup 15/15 pass. Full build-backed lint found one matcher typing error; exact identifier oracle replaces it and focused type-aware lint passes.
Propagate the V3 parent and current master CI reliability fixes without changing in-history semantics, performance budgets, or recorded predecessor ownership. PR-owned CLI seed and source-coverage repairs follow in their lower owning layer.
Retain the protected system-message fixture representation while incorporating the latest master PowerShell, browser timezone, asynchronous subscription and Python-runtime CI fixes through the V3 parent. Existing parent V3 successor refreshes are preserved without rewriting predecessor bytes beyond upstream master.
Local build and pwsh one-shot refresh passed. Persistent PowerShell completes the model turn but times out during process exit on both this layer and the updated V3 parent; an exact-master control is running before attributing that failure. PR-owned seed and coverage repairs are isolated follow-ups.
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.
Capture the inherited SDK PTC scenario under the upper loop lifecycle using the real current writer. The shared V3 dispatch vocabulary and protected system head remain unchanged; title follows admitted header/context as in the other upper recordings.
Regenerate current Python Session expectations through the real macOS arm64 SEA executable built from fdd44667b4b496093e765af57d3751dfb917d834. The native writer emits session/title after request/header and request/context. Update only five current V3 logs; result.json, minimal/model-visible.json, and minimal-in-history/prompt-history.json regenerate identically. All five V2 predecessors remain byte-identical to b2b300f9.
Build evidence: LEFTHOOK=0 pnpm install --frozen-lockfile --offline; pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets node24-macos-arm64 (full build, no skip-build): passed. file dist-exe/deepseek-harness-sdk-runtime-macos-arm64: Mach-O 64-bit executable arm64.
Smoke evidence: export PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=python/sdk/src:python/sdk-runtime/src; PY=/Users/cty/dsh-deploy/.agents/worktrees/sp-kv-sdk-evidence/tmp/py-sdk-venv/bin/python. For each scenario in sdk-snapshot sdk-restart sdk-minimal sdk-minimal-in-history, ran "$PY" scripts/smoke-python-runtime.py --exe dist-exe/deepseek-harness-sdk-runtime-macos-arm64 --scenario "$scenario" --update-snapshots, then the same command without --update-snapshots: all eight passed.
Helpers: PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=python/sdk/src:python/sdk-runtime/src uv run --offline --no-project --python /Users/cty/.local/share/uv/python/cpython-3.14.5-macos-aarch64-none/bin/python3 --with pytest --with "pydantic>=2.12,<3" python -m pytest python/sdk/tests/test_smoke_model.py -q: 28 passed. git diff --quiet b2b300f9 -- "scripts/snapshots/python-sdk-single-exe/**/*.v2.jsonl" and git diff --cached --check passed. No source, migration, test, or launcher changes.
Refresh both PowerShell native V3 cases with the installed PowerShell runtime so fallback title follows admitted request metadata consistently with the other upper-layer recordings. Refresh and independent replay both pass; historical inputs and output content remain unchanged.
Align the in-history ownership note with compact projection checkpoint version4. Both PRs use Session format3; the projection cache version is independent and invalidates scalar caches without introducing another persistence format.
Regenerate only native V3 Session/output oracles through the shipped headless and SDK profiles. These outputs retain system-message admission before users, bind request metadata before title fallback, and record real V3 delivery acknowledgments. Historical V0/V1/V2 replay inputs remain unchanged; pinned scenarios keep separate writer outputs.
The upper headless/SDK refresh covered109 cases; the only initial failure used macOS system Python3.9 for the PTC Python scenario, which passed with the declared CPython3.14 PATH. No assertion or runtime behavior was changed to accommodate it.
Forward the new headerless-window regression flag to replaceWindow instead of silently ignoring an extra test-helper argument. This makes the regression exercise a real incomplete history window and fixes the aggregate test typecheck without changing production behavior.
Give appended Chat system messages their own cards and suppress the following initial or same-step update header card. Preserve replacement/header ownership and conservative unknown surface ordering. Trajectory uses prompt-only presentation when no request header is loaded, never fabricated provider, model, or tools; prepend deduplicates against real request changes. Test-first regressions showed missing Chat cards and missing trajectory prompt data; 148 focused tests and affected client project typechecks pass.
The parent representation fix now correctly invalidates header-based caches with scalar state version3. The dependent in-history projection has a different compact live-surface schema and must not reuse that version. Advance it to4 and prove that a lower-layer version3 cache is discarded and refolded, alongside the existing version2 regression. No historical Session migration or compatibility mechanism is added.
Addresses ds-review-bot cache-version thread3921300994 across the stack. Validation:62 focused projection, adapter and admission tests passed; context.ts and breakdown-projection.ts each have100% exact-source coverage; paired README record updated.
Seed the request surface watermark from the attached session so a replacement before the first resumed request consolidates retained prompt versions. An unchanged resume still continues the series. Actual-loop replacement regression fails before the fix; 53 focused tests and the agent-loop project typecheck pass.
The normalized embedded stream in the skill seed contains no positive decode interval. Its rendered metrics correctly omit throughput rather than invent a rate. Refresh only the owning ARIA expectation after the full Web lane isolated this final mismatch; user/model/skill content and metric calculation remain unchanged.
Validation: owning refresh and subsequent read-only browser replay pass. The preceding full Web run had only this one expectation mismatch.
The in-operator narrows SessionEvent to events whose surfaceOp is required. Remove redundant undefined checks from both target Definitions; retain append-versus-replacement discrimination and shared prompt interpretation. This deletes dead conditions reported by the full type-aware CI lint gate without adding fallback behavior.
Validation: both Chat and Trajectory definition regression suites pass with the simplified predicates.
State the implemented empty-head reservation and messages-only summarization input in the feature note. Align the Chinese Chat contract with the shared surviving-surface interpretation rather than chronological predecessor selection. These localized corrections remove stale claims left by independently merged fixes without changing code or tests.
Validation: both bilingual pairs re-recorded and normal pairing hooks pass; the referenced runtime behavior is covered by the already executed empty-head, compaction and partial-window regressions.
Synchronous admission finalization moves the first request/header to seq10 before the deferred title. Read that actual header rather than seq11 request/context so the spill scenario exercises a large canonical request record. Keep the inherited dynamic-locator verification and independent file-content oracle; the test now requires a real successful read and SPILL_CANONICAL_OK rather than recording shell failure.
Validation: targeted built refresh and subsequent read-only replay both pass with complete spilled JSON equal to the live request/header and the session_event_search schema present.
The breakdown refactor correctly stopped consuming scalar shadow claims, but its old negative test was the only coverage of the pressure fold rejection path. Exercise the remaining real pressure consumer with mismatched start and end claims, preserving the runtime invariant rather than deleting or ignoring its guard.
Validation: Linux and Windows CI reported the two uncovered locations in surface-projection.ts. All118 token-meter tests pass and the exact pressure fold file now has100% statement, branch, function and line coverage.
Refresh advanced and restart expectations through the final native packaged runtime. Synchronous request finalization places deferred fallback-title events after header/context in parent and child logs; expected SDK notifications must reflect that actual lifecycle instead of forcing the old incidental scheduling order.
Validation: the advanced mismatch was reproduced against the built macOS ARM64 executable, both owning scenarios refreshed, then the complete keyless --scenario all passed, including the new in-history prompt evidence and independent shell/file effects. No SDK runtime or normalizer behavior changes in this commit.
Regenerate the source-owned service and persistence catalogs after prepared-route admission and clear semantics changed. Synchronize exact bilingual declaration blocks and replace the obsolete SDK evidence gap with links to the recorded TypeScript notifications and Python prompt history. Keep generated descriptions tied to the source instead of preserving the old previous-context decision rule.
Validation: both changed catalog/note pairs passed scoped pairing and exact type-equivalence/graph checks passed in the preceding isolated documentation repair. Complete documentation gates are rerun on the final integrated tree.
Request preparation now precedes durable user admission and finalization is synchronous. The title service intentionally defers fallback publication, so titles naturally follow request/header and request/context instead of landing in an incidental await gap. Refresh the actual headless, SDK, ACP and CLI recordings rather than adding a synthetic yield or changing title ownership to preserve obsolete ordering.
Keep every model response, user input, tool effect and identity relationship; review reordered title/header facts and remapped references. Current master also removes the base editor contribution, so shared schema pins follow the actual shipped profile. Historical generations remain unchanged: their test-only comparison orders title facts independently while preserving title payloads and references. Normalize only generated stream clocks in refreshed fixtures.
Validation: owning refresh runs passed current scenarios, CLI expected tier28/28, Web minimal/replay11/11 and targeted SDK in-history1/1. Read-only full built corpus passed127 cases with2 unavailable-PowerShell skips; its sole failure is the separately repaired historical invalid-citation negative control on the lower PR. Six retained historical comparison cases pass. No production behavior is changed in this commit.
Copy the current SessionEventMap prompt and route JSDoc into both subsystem references so type-equiv validates the prepared-route and clear-all semantics. Remove the Mermaid statement delimiter from the generated retry note while retaining the same lifecycle meaning; regenerate graph artifacts and confirm both bilingual pairs.
Validation: verify-type-equiv, verify-mermaid, verify-doc-graphs, scoped verify-translation-pairing, and git diff --check.
A durable system message may contain multiple text blocks or other content. The runtime-context helper recognizes only one text block; treating an unrecognized system message as empty allowed clearing and incapable-route normalization to leave its instructions active. Keep undefined text distinct from genuinely empty content so every noncanonical node is replaced or cleared instead of silently skipped. Runtime-context snapshot equality remains unchanged.
Regression: a multiblock head and nontext tail previously survived a clear request; the new source-level case fails before this fix. Both system-prompt projection and actual-loop admission suites pass (21 tests).
The system message content is readonly. Build a replacement fixture value for the multi-block extraction test instead of assigning into a readonly message property. Preserve the exact behavioral assertion without weakening production types or adding a cast that hides mutation.
Validation: aggregate client build exposed TS2540 at the mutation; all seven system prompt interpretation tests pass with immutable fixture construction.
The session projection API can return undefined when a unit is not registered. Assert the registered test fixture produced state before inspecting compact entries, rather than assuming a value that the current master type does not guarantee. This preserves the retained-state regression and adds no production fallback.
Validation: the aggregate build identified three TS2532 errors in this test; all context-breakdown projection cases pass after narrowing. Full build is rerun separately.
The meter regression deliberately rewrites the system node during request-error recovery. After the retry reconciliation fix, the next request must restore the captured admitted prompt rather than trust that temporary replacement. Keep the replacement stimulus and zero-delta metering assertions, but compare the retried prompt with the initial request so the two independently corrected components are tested together.
Validation: the combined regression run exposed this obsolete retry-text expectation while 945 other cases passed; all eight compaction-loop-repro cases pass with the admitted-prompt assertion.
Namespace prompt prefixes per invocation and compare appended updates against an identical warm request. Check exact pre-dispatch message prefixes and stopped responses; use the same latest prompt for rewritten-head control, without an artificial Updated prefix or assumed cache block size. This measures provider behavior, not loop capability selection.
Validation: serialize.spec.ts 54 passed; adapter.spec.ts 154 passed after offline dependency install resolved collection failures; focused e2e selector collected and skipped without standard key/model configuration; focused staged-config lint and git diff --check passed. No live provider result claimed.
Chronological lookup keeps B after compaction shadows it even though the model sees surviving head A and no system event is emitted. Share pure extraction and compact immutable surface interpretation through uiConversation; target-owned Definitions preserve historical cards. Track inherited surface positions in a map containing only surviving replacement endpoints. No full-log scans or prompt work on ordinary messages/chunks.
A partial window cannot infer the order of older unindexed endpoints: pre-window [head seq5, tail seq3], followed by replacements 3->6 and 5->7, must not report seq7 as effective. Withhold subsequent prompt text until prepend reconstructs the prefix; exact pure and both target regressions cover unavailable -> C. Drop shadowed replacement-index entries; repeated rewriting one node keeps one entry, while historical prefix maps remain immutable. Current replacement work is O(S+P), where S is surviving system nodes and P surviving replacement endpoints; history retains per-prefix copies, not an unbounded linked chain.
Coverage includes A->B->C, A->B->compaction->A without a system event in replay/live/partial windows, chained nonmonotonic replacements, empty tails, head rewrites, unknown ordering, inherited prompt withholding, and index pruning. Focused suites total 89 passing cases; rebuilt trajectory smoke 3 passed. Affected client types/lint/bundles, diff checks, and 33/33 doc-sync gates passed. README EN/ZH and owning note updated. Parent owns final GUI/recorded-session evidence; no server started.
Cover the missing SDK projections through shipped dsh profiles. TypeScript reuses the headless read-completion composition and records SDK notifications. Python all includes a focused minimal-profile variant with an ordinary prompt section updated after shell completion, unchanged tools, persisted/subscribed append events, and independently checked editor output.
Observed: focused TS refresh and built replay passed (1 test); Python sdk-minimal-in-history and sdk-minimal passed against apps/cli/lib/bin.js using uv; unchanged-prompt negative control failed as expected then restored scenario passed; corpus 3 tests, doc-sync 33 gates, and lint:contracts-ready passed. Host-only build initially left SDK built startup incomplete; build:lib:client completed the prerequisites. Native packaged/wheel and Windows validation remain pending parent integration.
The proposed and implemented 2026-09-02 feature triplets record the same decision. Keeping the proposal active presents obsolete node-0/config-change rules as live work even though PR3483 already shipped the capability. Delete the complete proposed EN/ZH/sidecar triplet rather than archive a proposal or retain a redundant rejection.
Read both complete bilingual notes before consolidation. The implemented owner already retains the motivation, all five proposal alternatives, cache cost and proxy/model risks, capability validation, and current snapshot/e2e evidence. Preserve the still-valid unchanged-prompt/resume lifecycle verification and both SDK typed appended-event expected-output requirement in its Testing paragraph. Explicitly identify missing SDK appended-event coverage instead of treating the headless snapshot as SDK evidence. Do not revive the obsolete plan-mode fixture or absolute cache-hit inequality; the implemented note records the shipped fixture and comparative e2e assertion.
Only the implemented Testing paragraph and pairing record change. Runtime, accounting, UI, and frozen archived notes are untouched. Active inbound references already target the implemented owner, so no link retarget is necessary. This commit addresses only the reviewed lifecycle/consolidation finding.
Focused evidence (all exit 0): pnpm install --frozen-lockfile; pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.md (1 record written); pnpm run verify-translation-pairing .agents/notes/implemented/feature/2026-09-02-in-history-system-prompt-replacement.md (1 named pair consistent); pnpm run verify-md-links (1381 files, all links/fragments resolve); pnpm run verify-agent-note-format (246 notes conform); git diff --check; git diff --cached --check. Full combined doc-sync remains with the parent integration branch after runtime fixes; no full gates, push, rebase, or hook bypass.
An in-history route can append A, B, and C without another request/header. The system-message Definition read only the prior real header, so C incorrectly reported A as its previous prompt. Select the newer real or synthetic header through the existing indexed predecessor reader; preserve target-owned Definitions and avoid any log scan.
Add replay and per-event live A -> B -> C regressions. Both fail before the fix with previous=A and pass with previous=B. All 13 trajectory Definition tests pass; focused staged lint, bilingual pairing, and diff checks pass. Update the trajectory README pair and owning design note.
Scalar shadow prices cannot restore an earlier effective prompt when compaction removes the newest system node: subtracting that price from messages can permanently make the published bucket negative. Reuse the positional measurement planner over compact current surface entries and choose the last nonempty system by surface order, not event sequence or provenance citations.
Explicitly trade the scalar checkpoint for O(current retained surface) state, dropping shadowed entries and message bodies. Version 3 invalidates old scalar checkpoints; pure transitions preserve wire identity for same-price rewrites. Cover newest/middle removal, reversed sequence ranges, extra sources, empty fallback nodes, replay and late registration, cache version rejection, and route-independent heuristic totals. Document the bounded-state exception and rejected prompt-only ancestry approach in both languages.
Use the same logged per-node normalization for an empty rendering as for incapable routes and broken request series: empty each active later system node, then empty the first node. Clearing only the latest node exposes stale earlier instructions, including converter-demoted user text. Dormant empty tails neither override effective text nor produce repeated replacements.
Keep the cleared structural head and ordinary restoration semantics: a capable continuing series may append new nonempty instructions, while incapable or broken-series requests refill the head. No initial empty-head creation change, deletion event, or rollback machinery.
Tests: capable/plain clearing after three active prompt versions fails before the fix and passes afterward; repeated clear and seeded resume remain empty without duplicate system events, restoration includes only new instructions, pi-ai conversion has no stale prompt, and source-event replay reconstructs every request. Final 395 loop tests pass with exact agent.ts/runtime-context.ts coverage at 100% all metrics; source tsc and focused lint pass. Fix new test discriminant narrowing discovered by focused compiler audit. Update EN/ZH consumers and owning rationale, seven pairs verified; parent owns broad gates and generated catalog/SDK/GUI updates.
Every same-step attempt resolves its bound route and reconciles the accepted rendered prompt against the current surface before deriving history. Preserve assembly, pre-step admission and entered users exactly once; compaction retries must not resurrect an older surviving prompt.
Normalize a broken request series to the current head plus dormant empty later system nodes, even if later versions survive or the effective text is unchanged. This removes the later-survivor exception without a delete operation and avoids appending instructions after already-admitted users on retry. Capture surface generation after reconciliation so subsequent ordinary retries do not emit phantom series headers.
Tests: both compaction cases fail before the fix (stale head, or stale later survivor), then pass with real loop/MockAdapter, pi-ai conversion and source-event reconstruction. Assert two retries keep one assembly/pre-step/user admission, preserve rendered text across section changes, and log one series boundary. Add unchanged-text explicit/tool series regressions. 391 loop tests with exact agent.ts/runtime-context.ts coverage pass at 100%; final admission spec has 11 passing cases. Source tsc and focused lint pass. Update EN/ZH implementation and owning rationale plus lifecycle retry documentation.
Resolve agent/request and prepareCall inside the accepted open step before committing system and user input. Preserve prompt-assembly model selection, bind capability and dispatch to the same prepared adapter, then log the envelope and derive the request synchronously. Preparation cancellation leaves a balanced empty step with no admitted input.
Normalize incapable-route retained prompt versions with logged per-node empty replacements and a current head, including unchanged rendering and resumed history. Dormant empty tails do not override the effective prompt. No new event or delete operation.
Tests: 389 agent-loop cases pass with exact agent.ts/runtime-context.ts coverage at 100% for all metrics; tsc -b packages/core/agent-loop/tsconfig.json passes. Seven actual-loop MockAdapter cases exercise pi-ai conversion, source-event reconstruction, both route transitions, resume, cancellation barriers, and bound selection. Stale-capability negative control fails all three transition regressions. Update EN/ZH request visibility, lifecycle generator, and owning rationale. Broad catalogs, recorded SDK/GUI and aggregate gates remain parent-owned.
Consolidate the existing in-history capability, loop, UI and artifacts into one baseline. Preserve the reviewed tree so each independently verified correction has a subsequent rationale-rich commit.
Begin each synthetic step before its user surface message so the structural V2-to-V3 migration can reserve its protected system head without reordering historical events. Keep the workload size, frame partition, original chunk provenance, and every timing/heap budget unchanged.
Add a small real persistence migration/publication/reopen prerequisite that preserves all user and assistant content and original V0 bytes. Both this regression and bounded stderr-headline regression fail before the fix and pass afterward. Worker errors retain a bounded head and tail instead of hiding migration refusal behind a generic stack tail.
Validation: full Session-opening benchmark16/16 passed, including all six128MB completion cases; focused full type-aware lint passed. First Agent resume median178.7ms and reopen27.4ms are below unchanged450/100ms budgets.
Refresh both current-writer successors with the real PowerShell runtime and existing headless composition. Align their owned prompt/schema pins with installed tools; retain every historical Session generation and the recorded model/tool behavior.
Refresh the inherited SDK PTC scenario through the native V3 writer so its protocol oracle includes the protected system head. The generated Session remains unchanged and independent replay passes.
Compose the shared release branch PTC rename with the structural system-head migration in the existing V2-to-V3 stage. Audit source payloads and remap local coordinates before renaming dispatch vocabulary and owned plugin attribution; retain original message identities and generation-qualified references. Native V3 validation combines the same system and PTC lifecycle semantics through a private released-validation view.
Reuse the V3 row-admission owner in current JSONL scanning before recoverable-tail suppression, so malformed rows cannot hide retired required PTC tags or header.system. The new corruption-prefix regressions and all173 existing JSONL scanner tests pass. Combined migration/catalog/persistence suite:300 tests; executable V3 migration source has100% coverage. No released generation, prior migration implementation, shared release branch, benchmark or CI workflow is modified by this integration.
Keep the generated configuration source pointers aligned with the smaller replay module. No configuration semantics or public runtime behavior changes.
The implemented owner at .agents/notes/implemented/architecture/2026-09-02-system-prompt-as-surface-node.md retains the decision, alternatives, consequences, and verification. Remove only the reintroduced pre-promotion triplet under the lifecycle rules in .agents/notes/README.md; the dependent feature proposal already links to the implemented owner. No proposal archival or body rewrite is needed.
Treat Vitest asymmetric matchers as unknown expected values and spell void assertion callbacks explicitly. This preserves the migration identity and refusal checks under the full type-aware lint configuration without relaxing validation or suppressing a rule.
Regenerate advanced and restart expectations through the shipped macOS arm64 SEA runtime built from 0521815d45. Real delivery-accepted events now record sessionFormatVersion 3 in the five V3 logs and advanced SDK notifications. Restart result.json and requests.json regenerate identically. Preserve all five V2 predecessors byte-for-byte from b2b300f9; no production, migration, launcher, or test changes.
Evidence (macOS arm64): pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets node24-macos-arm64 passed, including the full pnpm build. file dist-exe/deepseek-harness-sdk-runtime-macos-arm64 reports Mach-O 64-bit executable arm64.
For each SCENARIO=sdk-snapshot,sdk-restart: PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=python/sdk/src:python/sdk-runtime/src /Users/cty/dsh-deploy/.agents/worktrees/sp-kv-sdk-evidence/tmp/py-sdk-venv/bin/python scripts/smoke-python-runtime.py --exe dist-exe/deepseek-harness-sdk-runtime-macos-arm64 --scenario SCENARIO --update-snapshots passed; the same two commands without --update-snapshots passed.
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=python/sdk/src:python/sdk-runtime/src uv run --no-project --python /Users/cty/.local/share/uv/python/cpython-3.14.5-macos-aarch64-none/bin/python3 --with pytest --with pydantic>=2.12,<3 python -m pytest python/sdk/tests/test_smoke_model.py -q: 28 passed. Earlier helper invocations lacked pytest or SDK imports; corrected environment passed without source changes. git diff --quiet b2b300f9 -- scripts/snapshots/python-sdk-single-exe/**/*.v2.jsonl and git diff --cached --check passed.
Require a string call identity before constructing the private repair-validation prefix; do not stringify arbitrary durable JSON. Remove a redundant narrowed version comparison and use explicit void test callbacks.
Verified full type-aware package Oxlint (90 rules), all 71 focused cases with 100% exact V3 statements/branches/functions/lines, and host TypeScript build. No coverage ignores or broad lint exceptions.
Exercise malformed source envelopes, feedback metadata, file attachments, native system ownership, protected head replacement, started versus repaired tools, dangling coordinate mappings and successful compaction ranges. The focused package suite now has 71 cases with exact V3 source coverage at 100% statements, branches, functions and lines.
Remove unreachable rechecks after owned payload validation instead of adding artificial tests or coverage ignores. Unseeded inherited markers already fail at arrival; message arrays and IDs are validated before observation. Bind the unchanged frozen decoder run callback without an unreachable wrapper.
Construct supported historical inputs with the first surface inside step/start instead of weakening publication, singleflight, packed-row, and read-handoff assertions into refusal cases. Account for the inserted empty system head and shifted event sequence when V2 rows become V3; preserve exact native V3 expectations and original source bytes.
Keep structural system-message V3 recordings when taking the release branch spacing fix, rather than reintroducing retired header.system snapshots. Refresh native SDK delivery evidence and the retained multi-turn writer oracle; historical V2 inputs remain byte-for-byte inherited from the release base. Document generation-aware fixture storage checks so current child invariants stay enforced without rewriting retired historical roles.
Retained V0/V1 fixtures remain replay inputs while writer.expected JSONL records the actual current V3 output. Record these through the shipped headless scenarios rather than reverse-normalizing system messages back into historical headers. Preserve generation-qualified reference provenance, and correct the unreleased PowerShell seed to start its step and protected system head before user surface input.
Validation: the structural migration, real persistence, corpus and snapshot-support gate passed616 tests with one intentional skip; all91 headless refresh cases pass after the audited assistant-attempt source handling. Every predecessor path remains restored to the shared V3 base, and generated changes are confined to V3/output oracles and current tool schema pins.
Current V3 promotion must preserve chronology, so historical user surfaces before the first step are unsupported. Assert refusal for read and write opens, preserve source bytes and identity, and forbid intermediate or current publication. Frozen codec validation remains in its existing owner tests; V2 no-downgrade coverage lives in v2-system-migration.spec.ts.
V2 assistant attempts have no V0 semantic dispatch case: validate their exact V2 members and positive step coordinates locally while preserving the embedded stream. Explicitly classify agent-message relay attribution and file attachment metadata; both preserve foreign identities and non-Session byte counts, and reject unknown members.
Native V3 request headers retain current extension fields while rejecting retired header.system, independently of the closed V2 migration inventory. Add direct and multihop attempt regressions, exact source/file negative cases, and native header roundtrip coverage. All 41 focused tests and host tsc pass; scoped lint and bilingual pairing pass.
The structural V2-to-V3 edge reuses the frozen V0 payload and relationship helpers through their public package exports. Record the explicit workspace dependency so clean installs resolve the same validation code without editing a released migration package.
Stream an empty protected head after the first step and replace it before changed or cleared request headers, without moving source events. Remap only audited local sequence references, derive inherited cuts after upstream cardinality changes, and preserve generation-qualified captures and message identities.
Refuse pre-step surfaces and out-of-step prompt changes rather than invent lifecycle events. Reject unclassified migration payloads and detect deterministic generated-ID collisions in either source order. Native V3 accepts empty and in-history system messages, protects only the head, and reuses frozen relationships through a private nonescaping projection; historical repair IDs remain opaque.
Keep released codecs untouched and distinguish migration admission from native extension admission. V3 structural payload preflight cannot disappear behind recoverable row corruption. Validation: 35 focused tests, host TypeScript build, focused Oxlint, documentation quick gates and paired README recording. Parent owns installed-core/catalog/persistence integration and lockfile propagation.
Record order-preserving structural conversion and strict refusal instead of an identity edge; distinguish native writer layout and local references from historical delivery facts. Keep released generations frozen and one evolving unreleased V3 target, with canonical envelopes composing afterward.
Restore all 147 historical canonical fixtures changed by the lower branch to exact shared base 220ff708e3628a9be46d9747d09af6d8cd742f0d bytes (145 V2 and two V0). All 180 committed V0/V1/V2 canonical files now have zero diff against that base, including unchanged Python, test-support and preview sources.
Preserve corrected lower writer bodies in 146 V3 successors across 119 owner directories: replace 135 unreleased V3 counterparts and add 11 siblings. Change only each copied Session header version; preserve all body bytes, raw model streams, inline images, IDs, references and historical delivery generation markers. Remove two unshipped V2 additions after creating their V3 sibling: record-suite/rec-pin and empty-response-retry-current.
Keep six explicitly historical snapshot owners pinned with no V3 sibling. Restore the two pre-step V0 record child sources without synthesizing current output. Fix seven embedded Session header versions in exactly five test-support behavior owners whose output filenames already select V3. Leave protocol expectations, shared references, sidecars, consumer scripts and production migration/normalization code untouched.
Validation: purpose-built temporary Python inventory and verifier checked 337 canonical headers, 3714 successor JSON rows, message roles, contiguous child roles, exact successor bodies, all 180 historical base bytes and zero git diff, noncanonical JSONL immutability, six retained owners and six unchanged preview fixtures. git diff --cached --check passed. Temporary generators removed. Strict migration, native refresh and replay remain pending parent integration; unsupported pre-step sources are reported separately.
Use a typed expected failure object and the current toThrow matcher in the new leading-system-image regression. This preserves the rejection test while avoiding deprecated matcher and unsafe-any diagnostics from the full type-aware lint gate.
Address ds-review-bot thread 3921300994 (PRRT_kwDOS3Pfcs6eyLsu). contextBreakdown now folds system/message instead of request/header.system, so version-2 checkpoints have different semantics even though their numeric fields still pass the current schema. Bump the lower projection to stateVersion 3 so cache-only reads omit old values and restore replays the full log; derived caches are not covered by the Session-log persistence compatibility waiver. No migration or fallback is added.
Regression seeds a schema-valid v2 row at the current watermark with stale system/message prices. Before the bump, cache views returned it, restoreFloor selected seq 2 rather than 0, and restore retained system=0/message=17 instead of system=8/message=9. After the bump, the 13-test owning suite passes and exact src/breakdown-projection.ts coverage is 100% statements, branches, functions, and lines; refreshed rows equal a fresh fold at version 3. Existing token-meter README prose documents the current system/message fold without a version literal, so it needs no edit.
Propagation requirement: the upper layer already uses version 3 for a different compact cache representation. Advance that layer to version 4 when propagating this fix to avoid assigning one version to two schemas.
Address ds-review-bot thread 3921300999 (PRRT_kwDOS3Pfcs6eyLsz). The synchronous conversion removed a leading system message before its image check, silently discarding image-only, mixed, and nested image content. Reuse assertSupportedImageRoles on the unsplit history, matching the image-aware path without changing text prompt precedence or user-image storage requirements.
Regression: all three leading-system image cases failed against 8082f4a950 (expected rejection, received a context). The focused context suite now passes 20 tests, including both conversion paths, no attachment reads on rejection, empty/text leading prompts, later systems, and explicit options.system precedence. Exact src/context.ts coverage is 100% statements, branches, functions, and lines. Updated the paired README limitation and synchronous JSDoc.
These two current fixtures already contain system/message, so searching
only for stale request/header.system misses their persisted replay drift.
Both omit the shipped standard agentPreset. Plan exit must replace the
first system message (sequence 10), not append a second system prompt.
The Goal fixture's first get_goal result must reference its own tool call
at sequence 18 rather than the preceding bash call at sequence 16.
Refresh both owning browser scenarios. Retain the writer's canonical
embedded Assistant stream packing and refreshed timing where repacking
requires it. Expanded stream chunks, all 44 user/model/tool messages and
calls, and the deliberate shuf command failure remain unchanged. The
plan title remains before request/header on this lower branch. No UI,
runtime, normalizer, test-driver, or frozen v0/v1 files change.
Evidence at lower d29574fed9, reusing the dedicated tree's own prior build
(the intervening subagent spacing edits do not affect these owners):
- DSH_SNAPSHOT=replay pnpm exec vitest run --config vitest.web.config.ts apps/web/tests/{plan-review,goal-multi-turn-actions}.e2e.ts: before refresh, both owners failed persisted-session comparisons.
- DSH_SNAPSHOT=refresh pnpm exec vitest run --config vitest.web.config.ts apps/web/tests/{plan-review,goal-multi-turn-actions}.e2e.ts: 2 files passed, 4 tests passed, 1 record-only test skipped (10.24s).
- DSH_SNAPSHOT=replay pnpm exec vitest run --config vitest.web.config.ts apps/web/tests/{plan-review,goal-multi-turn-actions}.e2e.ts: 2 files passed, 4 tests passed, 1 record-only test skipped (10.24s).
- JSON payload comparison: all 44 messages/calls and expanded chunk content
across all 14 Assistant streams preserved; stream timing is excluded.
- git diff --check passed; only the two current session.v2.jsonl files changed.
Require exactly two System prompt controls for the unchanged-header resume fixture, and pin the repeated prompt in both collapsed and expanded subagent history. Keep all branch eligibility, disabled action, cancellation, disclosure, and inventory assertions intact.
Refresh stale July 25 clocks only where canonical fixture createdAt=0 already selects the seedSession near-now anchor. Preserve nonzero historical timestamps and the existing normalizer: do not conceal calendar differences globally. Update navigation read paths to the recorded workspace subdirectory and omit decode throughput for the zero-duration Bash cancellation stream.
Evidence: own frozen install and full build; baseline eight-file replay 10 failed, 37 passed, 3 record skips plus stale question teardown; six-file ARIA refresh 36 passed, 2 record skips; final read-only eight-file replay 47 passed, 3 record skips after separately authored Web fixture prerequisite eaa5277d82. Baseline exact count rejected 2 versus 1 and old date/prompt goldens rejected actual output. No Session generation, helper, runtime, or normalization edits in this commit. Live/question source mismatch is repaired solely by the prerequisite; their passing ARIA and skill goldens stay unchanged.
Fourteen current Web v2 fixtures still store the system prompt in
request/header.system and omit system/message. The built Cordis owner
passes its five behavior assertions but fails persisted-session replay.
Refresh thirteen owning recorded scenarios and update the authored pwsh
seed input to carry the same system text as a message event.
Keep the lower branch's title-before-header order. Preserve all 90
user/model/tool messages and all 35 recorded Assistant streams, including
timing. Only the system event, header field removal, message identifiers,
and their sequence references differ. Frozen v0/v1, UI goldens, runtime,
and normalizers remain unchanged.
Evidence on macOS, base 8d66f7c917:
- pnpm install --frozen-lockfile && pnpm run build: passed.
- DSH_SNAPSHOT=replay pnpm exec vitest run --config vitest.web.config.ts apps/web/tests/cordis-tool-round.e2e.ts: before repair, persisted replay failed after 5 behavior tests passed.
- DSH_SNAPSHOT=refresh pnpm exec vitest run --config vitest.web.config.ts apps/web/tests/{approval-composer,cordis-tool-round,feedback-command,file-upload-round,replay-round-trip,lifecycle-chrome,live-interactions,permission-policy-context,ptc-round,question-composer,steering,turn-tail-actions,web-search-round}.e2e.ts: 13 files passed; 71 tests passed, 2 record-only tests skipped.
- PATH="/tmp/pwsh:$PATH" DSH_SNAPSHOT=refresh pnpm exec vitest run --config vitest.web.config.ts apps/web/tests/pwsh-terminal.e2e.ts: 2 tests passed with existing PowerShell 7.4.6.
- PATH="/tmp/pwsh:$PATH" DSH_SNAPSHOT=replay pnpm exec vitest run --config vitest.web.config.ts apps/web/tests/{approval-composer,cordis-tool-round,feedback-command,file-upload-round,replay-round-trip,lifecycle-chrome,live-interactions,permission-policy-context,ptc-round,pwsh-terminal,question-composer,steering,turn-tail-actions,web-search-round}.e2e.ts: 14 files passed; 73 tests passed, 2 record-only tests skipped (102.62s).
- Exact base/current JSON comparison: all 14 files equal base plus the system event, header.system removal and deterministic ID/sequence remapping.
- No current Web v2 request/header.system remains; git diff --check passed.
Give JSON.parse results in the unexpected-header regression their actual record type before searching by event tag. Preserve the forbidden-field oracle while satisfying the full type-aware lint rules; do not weaken lint or production parsing.
Validation: exact-head Linux artifact CI reported unsafe-return and unsafe-member-access in this regression. The owning replay suite passes after the local typed result correction.
Replace the replay's fixed temporary-root find command with the existing fromRequest capture of the preceding session_event_read result. Quote the exact returned file and use portable grep -Fq/printf checks; emit SPILL_CANONICAL_OK only after request/header and session_event_search are present. Preserve the owning layer's seq11 request/header target.
Before cleanup or fixture refresh, require both successful tool results and the exact verification marker, read the returned spill file independently, and compare its complete JSON to the live request/header event. This prevents accepting a failed verification transcript or an unrelated/incomplete spill after event ordering changes. No runtime or error-text normalization changes.
Evidence: nonexistent recorded-root negative control fails at the marker assertion (bash exit 2 still carries isError:false). Focused keyless refresh, read-only source replay, built-profile replay, corpus ownership guard, and focused driver lint pass. Host/client library builds pass. Portable POSIX command reviewed; Linux CI remains the platform confirmation.
A resume header starts a visible request series even when the system node text is unchanged. Include resume in the request-prompt visibility predicate so full history and an older-page prepend retain the restart card. Keep startsSeries handling and existing regression assertions unchanged; align the local JSDoc and README pair.
Carry the newly merged current-writer spill recording through the representation change on its owning PR. Add the system surface message and remove header.system, while retaining the representation layer title-before-request order and every captured spill/source fact. The dependent admission PR separately records its changed title scheduling.
Validation: JSON records parse; the same recorded scenario was refreshed through the built integrated loop. Only layer-specific title order differs, and both variants are replayed before publication.
The source fixture now contains the system surface node, so a literal sequence13 no longer names its captured tail. Record the source watermark before saveText mutates the session and assert that exact independent observation. This continues proving the spill uses the pre-mutation capture without coupling the test to unrelated fixture event counts.
Validation: the newly merged upstream regression failed with expected13 versus actual14; the session-reference suite passes with the captured watermark assertion.
The historical-comparison negative control replaces title.messageSeqs with [0], which cites a non-human event. The current Session validator correctly rejects this before normalization; comparing the malformed result for inequality made the otherwise passing built snapshot lane fail.
Assert the specific earlier-human-message validation error for that malformed control. Keep the valid prompt and model difference comparisons, every citation field, and all production restoration and normalization logic unchanged.
Validation: full DSH_EXAMPLE_MODE=lib DSH_SNAPSHOT=replay pnpm exec vitest run --config vitest.snapshot.config.ts reproduced the lone negative-control failure, then passed all 6 files and 126 tests after repair (2 existing PowerShell availability skips). PATH used the existing uv Python 3.14.5 bin; no software installed.
The committed current rec-pin fixture held only request/header, so the corpus restore gate rejected it outside an open turn. Its owning scripted behavior already emits turn/start, step/start and the system surface message.
Regenerate the selected current fixture with ACP_SNAPSHOT_SPEC_BOOTSTRAP=1 and the focused rec-pin record test. Keep all retired parent and child generations unchanged. No parser, normalizer or production behavior changes.
Validation: the corpus restore test reproduced the missing-turn failure; documented owner bootstrap passed; pnpm exec vitest run packages/test-support/llm-replay/tests/session-format-corpus.spec.ts packages/test-support/session-snapshot/tests/suite.spec.ts passed (141 tests, one intentional skip).
The current Session relationship validator correctly rejects a second step/start while the previous step remains open. Both replay and refresh of the synthetic pin-turn scenario failed after the stricter master validation landed.
Add the missing step/end to the owning scripted behavior and resequence its later events. Regenerate only the current pin-turn fixture through defineAcpSnapshotSuite refresh mode. Preserve both system messages, replacement provenance, and all three request headers; do not relax migration validation or normalization.
Validation: reproduced both pin-turn failures with the focused suite filter; owning refresh passed; pnpm exec vitest run packages/test-support/session-snapshot/tests/suite.spec.ts passed (140 tests, one intentional record skip). Full build and doc-sync also passed before this fixture-only repair.
Retain installLlmReplay, Config validation and apply when resolving the streaming migration API change. The representation layer requires no replay plugin source changes, so restore its complete current-master implementation rather than a truncated conflict fragment. Capability additions remain exclusively in the dependent feature layer.
Validation: the restored source is byte-identical to origin/master and the replay unit suite passes. The initial normal commit hook failed because this detached worktree had no dependency links; pnpm install --frozen-lockfile installs them before retrying the unchanged hook, without bypass.
The default and retry header pins were retained pre-system-node logs, so preserving their historical header.system correctly exposed stale expectations across headless and shared SDK compositions. Move the default pin to tool-call-turn and add a current retry companion; retain every committed predecessor and keep readable sidecar ownership on text-turn.
Replace the historical blanket body-comparison skip with a headless-only semantic projection: current system nodes contribute historical header.system and event citations follow retained positions. Full normalized logs still compare, current-writer logs stay unadapted, and independent prompt/header checks remain strict. Negative controls preserve prompt, model and citation differences. Refresh only pi-ai metadata and Web minimal-preset current artifacts; no SDK outputs, Python fixtures or package oracles changed.
Validation: 18 focused headless/SDK/corpus tests; three pin, sidecar and historical mutation controls; Web minimal-preset persisted replay; focused oxlint; typecheck and client/Web builds. doc-sync:31 passed,2 failed on pre-existing token-meter/Cordis and config catalog freshness; no broad catalog rewrite. Full suite, browser interaction and platform matrix not rerun.
Regenerate source-owned catalogs after the oracle simplification and usage-anchor correction. The configuration catalog retains the same declared options while its source location follows removed compatibility stripping; the token-meter service docs now state that the usage anchor includes all admitted request inputs. Keep bilingual generated declarations aligned instead of retaining stale copied contracts.
Validation: pnpm run doc-sync identified only cordis catalog and config catalog freshness failures (31 other gates passed); pnpm run gen-cordis-catalog and pnpm run gen-config-catalog regenerated their owners, and the config pair was updated and recorded. Full layer documentation validation follows integration.
Cause: SystemPromptProjection skipped the first empty rendered prompt. The initial admitted user then occupied surface node zero, so a later nonempty prompt appended behind user history. Routes without in-history system support lost the leading system role; pi-ai demotes a non-leading system message to user content.
Fix: append the initial system node even when its content is empty. The existing loop commit order reserves node zero before admitted user messages; later prompt text replaces that node. Empty content still derives to no wire message. Keep retained-node replacement, clearing, multi-system handling, and pi-ai conversion unchanged; this addresses only the reviewed PR3476 initial-empty finding, not PR3483.
Tests: added initial-empty projection and two-turn loop regressions for empty wire output, reserved surface head, later leading system role, replacement intent, and series header. Negative control failed before the source fix. Focused projection/runtime-context/loop/request-reconstruction/session-surface/pi-ai-context suites passed 176 tests; exact runtime-context.ts coverage is 100% statements, branches, functions, and lines. test:docs passed all 15 gates. Updated README EN/ZH, architecture map and owning architecture note; recorded all three translation pairs. Broad doc-sync/lint stopped at parent request for combined-layer validation. No normalize.ts conflict-comment edit.
The representation PR left child and restart recordings in the old header-system representation while the packaged writer emits system/message before entered user messages. Restart result expectations also retained standalone assistant/chunk notifications after the writer moved stream records into assistant/message. These are stale expected artifacts, not fields to erase in normalization.
Regenerate the owning advanced and restart scenarios through the native macOS ARM64 packaged dsh runtime. Retain messages, tool effects, typed feedback and packed streams; update only the missing system nodes and their sequence references, and remove obsolete standalone chunk notifications. The advanced parent result and parent session already match the writer after the rebase, so this commit changes only two child logs and the restart result/logs.
Validation: pnpm run build; pnpm exec tsx scripts/build-exe-for-python-sdk.ts --skip-build --targets=node24-macos-arm64; uv run --project python/sdk python scripts/smoke-python-runtime.py --scenario sdk-snapshot --exe dist-exe/deepseek-harness-sdk-runtime-macos-arm64 and the equivalent sdk-restart command each reproduced the mismatch, then passed with --update-snapshots and again without it. Both read-only reruns pass. Other native targets remain covered by exact-head CI.
The rec-child fake-agent behavior used an 11-digit UUID tail for both copies of its shared system message. Supply the missing digit in both places without changing their identity relationship. This semantic correction is separate from the preceding five-file formatting-only change.
Add a focused owner-fixture assertion requiring two matching complete v4 UUIDs. It fails on the original short tail and passes after repair. Evidence: pnpm exec vitest run packages/test-support/session-snapshot/tests/suite.spec.ts: 140 passing, one intentional record-mode skip, including replay/record/refresh and UUID validation. No expected-output refresh or normalization change is needed.
Compact each logged event onto one line in exactly five owner-local fake-agent behavior fixtures: record-suite rec-child/rec-pin and suite pin-turn/plain-turn/shared-pin. Keep wrapper structure readable so event sequencing and payload changes remain reviewable without hundreds of formatting-only lines.
Evidence: node deepStrictEqual compares parsed working-tree JSON against HEAD for all five changed files, with exactly five paths asserted; all semantic values and ordering are identical. git diff --check passes. The malformed rec-child UUID is deliberately retained here for a separate semantic repair commit. The unchanged fixture behavior passed the preceding suite.spec.ts run (139 passing, one intentional skip). No snapshot refresh was run.
Delete the leftover diff3 parent marker and both copies of the obsolete request-header prompt JSDoc. Keep only the current system/message tokenization contract. This is a local comment-only finding; neither oracle behavior nor fixture content changes.
Evidence: git diff --check is clean; focused normalize.spec.ts scrubSystemPrompts test passes (1 selected, 64 skipped). The earlier header-preservation commit intentionally retained this marker so the findings stay independent.
sessionFixtures already selects the highest generation independently for each parent or child role. Filtering its output to files[0] silently exempted every child from prompt and schema fixed points and prompt-before-request ordering. Merge those assertions into the per-role loop and expose the actual checker for focused negative controls without mocking Vitest registration.
The three selected-child controls reject missing system/message, raw prompt text, and raw tool schemas; each resolved incorrectly with the parent-only filter. A positive mixed-generation case proves retained predecessors remain unselected. Correct the two versionless record-suite child fixtures that the restored enforcement exposes, including the retired-child copy used by recording tests. No released historical generation or broad recording is rewritten.
Evidence: pnpm exec vitest run packages/test-support/session-snapshot/tests/storage-policy.spec.ts packages/test-support/session-snapshot/tests/suite.spec.ts --coverage --coverage.include=packages/test-support/session-snapshot/src/suite.ts: 143 pass, one intentional record-mode skip, suite.ts 100% statements/branches/functions/lines. Update and re-record the session-snapshot README EN/ZH pair.
The loop appends step/start before system/message and the entered user
messages. Capturing nodes at step/start therefore omits inputs already
included in the provider's successful usage, then adds those inputs back
as a positive surface delta. Prompt replacement can also incorrectly add
or subtract the difference from the prior prompt on a completed call.
Snapshot the current priced surface immediately before assistant/message
commits. Keep provider output separate from the durable assistant node so
listener rewrites retain their signed delta. The invariant is zero delta
immediately after an unchanged successful output: provider usage already
includes every admitted prompt input. Later appends/replacements still
produce signed deltas, and low or absent usage keeps heuristic fallback.
Delete stepStart.nodes rather than adding prompt-specific corrections or
another request snapshot: the existing transactional surface fold already
contains the successful request inputs, including replacements made during
same-step retry recovery. Keep turn/step state and all overlap, mismatch,
and late-assistant lifecycle validation. Retry attempts are log-only and
request middleware changes configuration; injected messages remain queued
until admission. No loop, event format, projection, or retry policy changes.
Exercise the real loop with reported usage and initial, growing, shrinking,
and empty prompts; same-step failed attempt plus retry prompt replacement;
request middleware; eager observation and fresh seeded replay. The two
regressions fail before the fix with spurious deltas of +48 and +18 tokens.
Retain existing durable-output rewrite, route repricing, missing/low usage,
transactional failure, and lifecycle tests. Update README EN/ZH and the
existing system-prompt surface-node Agent Note, including pairing records.
Validation (dedicated worktree, no full unit suite):
- pnpm exec vitest run packages/llm/token-meter/tests packages/compaction/compaction-basic/tests/compaction-loop-repro.spec.ts --coverage --coverage.include='packages/llm/token-meter/src/index.ts'
118 passed; exact changed runtime file 100% statements/branches/functions/lines.
- pnpm exec vitest run packages/core/agent-loop/tests/request-reconstruction.spec.ts packages/compaction/compaction-basic/tests/compaction-basic.spec.ts packages/compaction/compaction-basic/tests/loader-composition.spec.ts
118 passed, including retry reconstruction and real Loader composition.
- pnpm run doc-sync: 33 gates passed.
- pnpm run test:docs: 15 gates passed.
- pnpm run lint: passed, 0 warnings/errors.
- git diff --cached --check: passed.
Baseline normalize.ts comment conflict marker is intentionally untouched.
The session already derives the protected system head as a Message, and both adapters accept leading system history. Passing it through a separate SummarizationInput.system string unnecessarily flattens that value and rebuilds the same wire message in the adapter. Prepend the derived head to messages and remove textContent, the separate field, and GenerateOptions.system plumbing from the summarizer.
Keep range selection, shadowed seq accounting, session head protection, routed tools, image references, target policy, and the model-visible compaction instruction unchanged. Empty-content heads still derive to null and contribute no request message, but their surface node remains protected. Update subclass consumers/tests, EN/ZH package and subsystem prose, and the existing system-prompt surface owning note with refreshed pairing records.
Evidence: pnpm exec vitest run packages/compaction/compaction-basic/tests packages/llm/llm-deepseek/tests/serialize.spec.ts packages/llm/llm-pi-ai/tests/context.spec.ts --coverage --coverage.include='packages/compaction/compaction-basic/src/region.ts' --coverage.include='packages/compaction/compaction-basic/src/summarizer.ts' passed 203 tests in 6 files; both changed sources have 100% statements, branches, functions, and lines. Region-to-default-summarizer cases pin exact prefix and tools for nonempty Unicode/multiline, empty, and absent heads. DeepSeek JSON byte equality and pi-ai context equality pin leading-message vs separate-system equivalence on text and image-capable conversion paths.
pnpm run doc-sync passed all 33 gates including doc-typecheck, documentation build, translation pairing and model-experience checks. git diff --check passed. Own dependencies installed with pnpm install --frozen-lockfile. An initial test iteration used a nonexistent ctx.dispose teardown on the in-memory fixture; corrected to its existing fixture lifecycle and reran successfully. No runtime/model behavior, normalizer marker, main worktree, push, or rebase changes.
Remove both unconditional header.system erasures from log normalization and replay comparison encoding, including the unrelated Session-header deletion. The catalog owns released-format migration; comparison must not turn an unexpected field into equality or supply a compatibility shim.
Add direct log/snapshot and catalog-restoration controls that retain unexpected request/header.system and differ from the field-absent fixture. Preserve malformed provenance data as well, closing the owned normalizer coverage gap. Update the two package README pairs with this oracle obligation.
Evidence: pnpm exec vitest run packages/test-support/session-snapshot/tests/normalize.spec.ts packages/test-support/llm-replay/tests/llm-replay.spec.ts --coverage --coverage.include=packages/test-support/session-snapshot/src/normalize.ts --coverage.include=packages/test-support/llm-replay/src/index.ts: 205 tests pass, both files 100% statements/branches/functions/lines. Direct normalizer negative control fails before the fix. Baseline diff3 comment remains untouched for the separate cleanup finding.
Consolidate the representation-change PR and its rebase reconciliations into one baseline. Preserve the exact tree and keep the in-history feature in the dependent PR. Follow-up fixes remain separate.
Master's windows-python-console-spawn-wait fix removed the plugin-add flake
this branch previously retried, so that retry is dropped entirely; the
branch keeps the changes that remain useful.
- Retry the immutable install step up to three attempts total with a
ten-second pause (bash on every platform): node-gyp's Node-header
download from nodejs.org intermittently times out, as observed on the
hosted node24-macos-x64 cell.
- Keep checkout credentials out of the build tree (persist-credentials:
false) so dependency-install scripts cannot read the embedded token.
- Failover runbook: drop the stray leading '#' before the Dependabot
paragraph on both sides.
- Hosted-runtime note: name the PR/commit ownership and UTC timestamps of
the cited runs and record the constraints a future self-hosted attempt
must satisfy; new note records the install retry decision.
The failover runbook's Windows switch scope returns to the native Windows jobs:
remove the Python runtime eligibility sentences, the pool Python prerequisite,
and the proposal links. The #3629 self-hosted Python runtime proposal is
retired (deleted as obsolete after its unmeasured-throughput run), and a new
implemented note records that the Windows x64 runtime lane stays on
GitHub-hosted Windows with the evidence and alternatives. Bilingual sidecars
re-recorded.
The failover runbook's Windows switch scope returns to the native Windows jobs:
remove the Python runtime eligibility sentences, the pool Python prerequisite,
and the links to the proposal. The self-hosted Python runtime proposal is
moved to rejected with its unmeasured-throughput verdict, and a new
implemented note records that the Windows x64 runtime lane stays on
GitHub-hosted Windows with the reasons. Bilingual sidecars re-recorded.
Windows x64 runtime builds resolve their hosted matrix.runner unconditionally
again (windows-2025 for pull-request CI). Remove the DSH_CI_FAILOVER_WINDOWS
selector, job-private Python toolchain, self-hosted dependency install and
post-step cleanup, the private setup script, and the routing spec introduced
in #3629. The Windows failover switch again covers only the native Windows
jobs in ci.yml.
Consolidate the existing notes-only PR into one review baseline. Preserve the exact reviewed tree; subsequent corrections remain separate commits with their own rationale and regression evidence.
Reflect the review feedback on the reader list: assistantStreamFirstTokenTime and the has-visible readers stop at the first qualifying member, while lastAssistantStreamChunk, assistantStreamChunks, and joinAssistantStreamText scan the whole stream; assembleAssistantStream feeds a BlockAssembler one joined delta per run. Record-level readers trust the static record type; expandAssistantStream is the validating path.
Session format v2 embeds each attempt's compact stream in
assistant/message and assistant/attempt, but Host and client consumers
still expanded it into per-member TimedStreamChunk arrays and did
per-member work; expandAssistantStream materializes the full array before
find/toReversed/break can answer. Session Stats (the projection phase of
every Session open), the token meter's usage and provider-assembly folds,
the subagent output fold, and the Session Controller image lookup still
paid O(members) allocation and time per settlement.
The Chat and Trajectory definitions were already settled from
message.content on master; the remaining per-member folds stay.
dsh-llm now exports record-level readers (first token, visible content,
visible text, last raw chunk of a type, raw chunks of a type, joined
text, run-aware assembly, per-run first-token/first-visible times) that
scan the compact records once with early exit. Session Stats reads
assistantStreamFirstTokenTime, the token meter reads
lastAssistantStreamChunk(stream, 'usage') and assembles through
assembleAssistantStream, the subagent output fold appends
joinAssistantStreamText, and the Session Controller scans
assistantStreamChunks(stream, 'block-end').
expandAssistantStream is deliberately not memoized: retaining expansions
costs roughly ten times the compact stream for the Session's lifetime.
It remains the validating path at durable boundaries.
Synthetic 200-turn v0 migration benchmark, median of five: first-open
projection 28.0 ms -> 5.4 ms, first-open total 76.9 -> 50.0 ms, peak RSS
137.2 -> 94.9 MB; reopen projection 17.8 -> 5.6 ms; all phase budgets and
the 128 MB heap constraint keep passing.
The migrating first open measured 1,963 ms on the CI runner under the
128 MB heap limit; 4,000 ms keeps a 2x margin while the repeated-snapshot
implementation still fails by heap exhaustion and would need ~10 s.
A records-proportional fold still joins 20x more text in the large window,
which measured about 2.5x the small fold; the per-delta replay measured
about 11x. A 5x bound separates the two on a noisy runner.
The migrating first open of the synthetic log costs about 1 s on the
reference machine under the 128 MB heap limit and about twice that on the
CI runner, so 3,000 ms keeps headroom while staying far below the ~5 s
(~10 s on CI) of the repeated-snapshot implementation.
scripts/test-proxy-environment.spec.ts pins the Vitest configs that declare
setupFiles so the proxy isolation setup is never dropped; the benchmark
lane declares it too.
Add a benchmark lane (`vitest.bench.config.ts`, `pnpm run test:bench`,
gate mode `ci-bench`) and a required `node 24 / benchmarks` CI job that
runs it alone. Benchmarks synthesize their input in-process from fixed
parameters and fail on documented budgets:
- `open-generation.bench.ts`: a 200-turn released-v0 log with 500 text
and 125 reasoning deltas per reply (127,400 events, ~2.8 MB) encoded
through the frozen v0 codec; the migrating first `open()` must finish
within 2,000 ms in a child process capped at 128 MB of old space, and a
fresh process must open the published current generation within 500 ms.
- `conversation-fold.bench.client.ts`: 200 replies whose compact streams
hold 2,000 text + 500 reasoning deltas each, folded through every Chat
Definition by the real assembler; the fold must finish within 150 ms
and stay within 3x the fold of the same window with 100 deltas per
reply.
On this commit both gates fail: the migration exhausts the 128 MB heap
(4.8 s and 696 MB peak RSS without the cap; the pre-stack decode of the
same bytes took 34 ms and 168 MB) and the fold scales 11x with the delta
count. The stacked fixes bring both paths to O(records).
Replace the external ArrayBuffer view and manual UTF-16 scan with native Koffi decoding through a pointer-width buffer. Preserve the original address for CoTaskMemFree.
Exercise production resultPath with real Koffi decoding of live UTF-16 buffers. Cover zero-low-byte BMP characters, surrogate pairs, NUL termination, and strings exceeding 32 KiB; test four- and eight-byte BigInt pointer preservation and the original release address. A negative-control mutation confirms truncating a 64-bit address fails.
Keep the bilingual Agent Note focused on implementation rationale and required memory ownership. Preserve master archive seals during rebase.
Co-authored-by: zhanglunet <[email protected]>
`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.
Archiving low-future-value Agent Notes (#3548) stripped the inbound
citations to three now-archived notes from the ui-primitives README, and
this branch rewrote the same "Controls and icons" opener so the new
component catalog owns the export enumeration. The resolution keeps this
branch's opener and drops the archived-note citation master removed, in
both languages; the pairing record is re-recorded from the merged pair.
The track's radius is half its height, so its corners consume the whole
side and the global `superellipse(1.5)` squares the ends off — beside a
thumb that already opts out at `border-radius: 50%`, leaving a round knob
in a squared-off slot. Pair `corner-shape: round` with the radius, the way
every full-round radius in the repository does.
The corner-shape stylesheet spec recognizes 50%, 100%, and radii at or
above 99px, so a radius that is full-round only relative to its own box
passes it; the comment records that gap at the rule it affects.
The `idle` addition left two maintainer-facing statements behind: the
README's limitations entry still listed four supported StateDot states,
and TerminalBlock's run-state JSDoc still counted the union as four while
correctly saying three of them are reachable there.
Pill's header comment still sent every capsule without `onClick` to Tag,
which contradicts both the catalog this pull request corrected and
TerminalBlock's own static read-only Pill two files away. Size separates
the two controls as much as interactivity does, and the comment now says
so. The function JSDoc and the static-mode tests stay as they are: they
describe supported behavior.
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.
`unloading` marks a teardown the Host is running, which an async disposer
can hold for a while, so the inventory's dot animates it like `loading`
instead of showing the static grey `idle` mark that means no activity.
Correct four documentation facts against the shipped code: StateDot's
render JSDoc still promised four states, the catalog claimed four Tooltip
sides where TooltipSide has three, the Agent Note's geometry audit counted
three matching rules where two match, and the migrated-site and shipped-
appearance counts had drifted.
Record two things the first pass left out: TerminalBlock renders its exit
status as a static Pill, which the Tag/Pill catalog entry now accounts for
by size rather than by interactivity alone, and the disabled inventory tag
changed fill token, which joins the visual-change list.
Pin Tag's eight tones with a stylesheet spec, for the same reason StateDot
has one: CSS Modules resolve to class-name maps in component suites, so a
missing or wrong tone rule passes every render assertion.
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.
- Stop treating three archived records as current authority: bounded-llm
citation now points at the llm-retry README, the semantic-composer
proposal cites the runtime-owned child guard as history it builds on,
and the code-runtime comment states the deferred rationale locally.
- Fix the stale "dsh family stays restricted" claims left without their
rationale citation: the check-workspace-constraints comment, the
release publish comment, and the kept npm-release-sequences note now
record that the family published publicly on 2026-08-13 (87f8e6a728).
- The rejected client-settings-locale-theme body keeps its pristine
proposal prose with only the Status line and the two link paths whose
targets this PR archived; the link remaps are required for
verify-md-links and pairing, and every prose byte is untouched.
Run the dsh-archive-agent-notes audit over every active Agent Note on
current master, judging each record by whether its rationale still guides
work rather than by size or age.
- Archive 453 implemented bilingual triplets (417,882 English words):
completed UI chrome, narrow adapters, closed bug fixes, implementation
walkthroughs whose package READMEs, docs pages, generators, or successor
notes now carry the useful behavior, and 51 records fully superseded by
a later active note. Keep 201 implemented notes whose ownership rules,
negative guarantees, durable or wire semantics, security rules,
reintroduction conditions, or still-tempting rejected alternatives
remain useful.
- Reject 7 proposals whose premise is gone or whose work shipped in
amended form under other records; delete 2 rejected notes that no
longer prevent a plausible mistake.
- Retarget every remaining inbound link to the archived path, and repair
active prose that named an archived record as the owner of a live fact:
parenthetical citations drop, ownership sentences redirect to the
README, docs page, or active note that states the fact, and history
citations say so. Chinese files link the English archived path because
the pairing gate treats the frozen tree as outside the bilingual corpus.
- Seal 1,359 new frozen artifacts; existing seals are unchanged and
outbound links from archived notes are neither inspected nor repaired.
- Regenerate docs/config-catalog.md after the hook-bridge comment edits
shifted two source line numbers.
Client feature plugins never import one another's components, so
ui-primitives is their only channel for sharing one. Nothing told an
author to look there first, and the package offered nothing to look
at: its README named six source files against forty-plus exports.
Add Tag (read-only capsule badge, eight tones) and Switch (36x20
toggle with a required accessible name), extend StateDotState with
idle for a tracked subject with no activity, and move ui-agent-preset,
ui-settings-plugins, and ui-settings-plugin-inventory onto them.
The plugin inventory's conditional tag referenced
--dsw-alias-state-warning-primary, which does not exist, so both
themes had been painting its hardcoded #b45309 fallback.
State the reuse rule as the first step of the new-component checklist,
and give the README a component catalog so the rule has something to
check. The rule is guidance, not a gate: a package may still write its
own control, and the Agent Note records the five that stay local.
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 Agent Note rules keep the `# Agent Note: ` title prefix and the
`Status:` line in English verbatim on both language sides. 51 Chinese
counterparts carried a full-width colon (`# Agent Note:`), an annotated
prefix (`# Agent Note(agent 决策记录):`), or a translated status token
(`状态:已实现`, `状态:implemented`, `状态:已实施`, `状态:proposed`), which the
archive verifier rejects on entry to the frozen tree. Normalize the tokens
and re-record the bilingual sidecars; no body text changes.
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 heuristic tools segment counts tool schemas only, so the row reads 工具定义
/ Tool definitions; the header figures lose the ~ because they are anchored to
provider-reported usage, unlike the heuristic rows that keep it.
The default and hero placeholders drop the ellipsis and name the / commands
and @ references as comma-separated actions; goldens and e2e assertions
refresh with the copy.
Tighten the composer card's top pad and draft-text pads, pull the attachment
rail closer to the draft, settle the toolbar gaps on 12, give the hero
workspace row its right clearance, and let the wrapped hero badge center under
the title with more air between the wrapped fish and the title line.
ModelSelect renders its dropdown through a body portal with viewport-clamped
fixed placement, so the panel keeps its full width instead of being cut by the
sidebar edge. The preset seat passes Menu an anchor class with an icon-only
min-width floor and moves its label ellipsis onto a dedicated span, keeping the
chip's overflow degradation breakpoint-free. Toasts move to the viewport top
and shrink to fit their text.
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.
Clickable artifact links across the transcript (markdown anchors, prose
file mentions, web source/fetch links, produced-file chips, workflow
member links) now share one language: a dedicated --dsw-alias-link color
(light deepseek-500, dark deepseek-400) decoupled from
state-business-primary, font-weight 500, no underline at rest, and a
dotted 3px-offset underline on hover/focus. A new ui-primitives LinkIcon
leads each link with a currentColor category glyph (url / folder / code /
image / document / other) derived from the path extension by
classifyLinkPath. Produced-file chips drop the grey pill and 96px cap and
render at natural width, shrinking with ellipsis only on row overflow.
ToolRow's grey dotted file links and the grey show-in-folder action stay
as they were. The inline-code chip tint moves to neutral-50 (dark
neutral-800) with its 0.5px border kept.
Coverage: LinkIcon unit spec, refreshed markdown-dom fixtures, and the
clickable-links-gallery web e2e registered in the host compiler face.
Closes#3546
Stop re-exporting the verbatim file-store helpers from attachment-local, move
the raw-byte upload route handler into a non-entry module of file-upload, and
drop the redundant hook type re-exports from the Client entry. The route test
moves next to the module it exercises.
Write handles now hold a durable lease (session.lock.json beside the
log): a random owner token, diagnostic pid, and an expiry. Acquisition
wins by exclusive create; a second process's create or write open rejects
while the record is unrenewed for less than leaseTtlMs (default 5 min),
and takes over after that — a crashed holder is waited out, never
reclaimed by pid. The holder renews every leaseRenewIntervalMs (default
4 min); a renewal that finds a foreign, vanished, or expired record — or
fails outright — marks the lease lost permanently, so every later
append/flush rejects with SessionOwnershipLostError while reads continue.
Close releases the record; read handles never touch it. Takeover of an
expired record is eventually exclusive: a replaced holder stops within
one renewal interval.
Refs #3245
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.
The dsh-family version rule lived only in the packages/ block of
checkWorkspaceManifest, so apps/ members and the root-named CLI carried the
shared version with no static check of their own. The name-based rule now
covers every scanned manifest; regression coverage names the stale-version
merge that shipped packages/util/http-proxy at 0.1.2-alpha.5 behind a
0.1.2-rc.1 family.
`SESSION_FORMAT_VERSION` is the only value that names the Session format
generation. Two other version fields had moved to 2 alongside it:
- The `dsh_session_log` request extension's outer schema `version` returns
to 1 and its Session header projection keeps `seedLength`, derived from
the logical inherited cut, so the external wire is unchanged in this PR;
`sessionFormatVersion` alone identifies the embedded generation.
- The `sessionStats` projection unit's `stateVersion` returns to 1: the
projection cache binds every checkpoint to the format generation, so a
generation change discards old rows without a unit version bump.
Test doubles and the standalone Client fixture that build a current
header now spell `SESSION_FORMAT_VERSION` instead of the literal.
The README file table listed four of the six source modules and omitted
the two largest, so the package read as an oversized identity conversion.
The summary now states that most of the source is the frozen released
v0/v1 event vocabulary, why the edge refuses a malformed historical log
before the installed current restorer runs, and that later edges reuse
those shapes; the table gains `payload-validation.ts` and
`relationships.ts` rows.
The `start` frame and the reconnect baseline attempt carried a wall-clock
`startedTime` that no Host or Client consumer read: the Host accumulator
and the Client reconciler copied it into their attempt maps and nothing
looked at it again. Presentation frames now carry only the coordinates
consumers use (attempt, revision, turn, step, chunk index, and v1 seq
provenance); the type-equivalence block, event graph, READMEs, and the
live-stream Agent Note follow the type.
`session[.vN].jsonl` was assembled independently by the JSONL provider
(twice), the session-log export archive, and the recorded-session fixture
helpers. `dsh-session-format` now owns `sessionFormatLogFilename` and
`parseSessionFormatLogFilename`; the three consumers append only their
compression suffix, and the migration note names the owner.
`snapshotSessionFormatJson` re-implemented the lossless JSON walk that
`dsh-util-values` already publishes as `snapshotJsonValue` + `deepFreeze`,
and the frozen v0 relationship validator carried a third structural JSON
comparison next to `deepEqualJson`. Both now delegate; the format package
reports one `is not lossless JSON` diagnostic per labelled subject instead
of ten member-specific ones, and enumerable accessors whose values survive
a JSON round trip are accepted like `JSON.stringify` accepts them.
`assertFixtureInventory` folds every retained generation of one parent or
child fixture into a single role before comparing, so an older generation kept
beside the current one is not an inventory drift. The generation spec writes
headers that match each filename, since fixture selection validates the chosen
generation, and asserts that an absent parent resolves only for an
override-only replay. The `dsh_session_log` composition expectation follows the
extension's version 2 payload.
The continuable-child headless expectation and the Python `advanced` and
`restart` scenarios pin the current runtime, whose subagent children carry no
`session/end-seed` marker and whose assistant messages embed their streams:
the child fixture drops the marker and renumbers its references, `advanced/`
re-records `result.json`, and both scenarios gain their v2 Session fixtures.
Unit tests cover the `tool-call-delta` id and name validation, embedded
stream members without timing arrays, and the versionless-header refusal.
The JSONL backend rethrows storage errno failures and abort signals from the
source read of a pending migration unwrapped; a test now drives both through
the stat seam so the classification stays covered. The navigation-panes Web
e2e derives the exported archive entry from `SESSION_FORMAT_VERSION`, which
`session-log-export` uses for its `session.v<N>.jsonl` name. The module graph
records the `session-log-export` → `session` peer edge.
`session-format-v1-to-v2` takes the `0.1.2-rc.1` root version that master
requires, and `defineReleasedPayloadDisposition` drops the `@param` tag for a
parameter it no longer has.
`session-log-export` names its archive entry after `SESSION_FORMAT_VERSION`,
a runtime value of the shared `dsh-session` instance, so `dsh-session` becomes
a peer dependency and the export is classified as peer-required in the package
dependency policy. The three Session format packages take the `0.1.2-rc.1`
root version that master now requires.
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 repository-only benchmark, its unit-suite spec, the root script, and the
README section are removed; the accepted acceptance run's figures stay in the
embedded-assistant-streams note as recorded facts. The note also records why
the migration edge reuses the `dsh-llm` stream helpers instead of frozen
copies and why its target validation re-checks message/stream agreement
itself. A validation test that recomputed `RELEASED_V2_EVENT_TYPES` from the
same expression as its definition is dropped.
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.
`ensureJsonlGenerationCurrent` returned a live Zstandard body iterator that
every production caller disposed unread before decoding the same bytes again;
the header read now decodes only the first frame and owns no decoder past its
return. The JSONL backend reuses the generation module's stable-revision read
instead of keeping a second copy, `encodeCurrent` encodes the validated
current artifact it is documented to receive instead of re-running the
migration chain on it, and the test-only `inspectVersion` catalog method and
the `migrateSessionSnapshotFixture` duplicate of
`prepareSessionSnapshotFixtureForComparison` are removed.
Second review round (ds-review-bot v4p/v5/v6): the exit cleanup claim that
collectors unlink their spill files on dispose was wrong — completed spill
files are retained (seal() only closes, discardSpill() is the only unlink),
so the exit removal only ever applies to directories that never spilled.
Reword the JSDoc, README, and Agent Note accordingly, drop the redundant
readdirSync precheck (rmdirSync ENOTEMPTY is caught anyway), and recount the
v8-ignore window. The spawn.spec default-spill test now records the default
per-process directory it created and removes it in afterAll, so a completed
spill file is never left behind by the unit suite.
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 Agent Note now links the 2026-07-17 local-spill-startup-cleanup note it
builds on, and the privateSpillDir JSDoc documents the empty-dir exit
removal alongside the directory's creation contract.
Review (ds-review-bot) found the exit-time deletion of per-process spill
roots conflicts with the documented retention decision: spill artifacts are
model-visible locators that resumed or forked sessions may still reference
(2026-07-17-local-spill-startup-cleanup), and an exception thrown from an
'exit' listener can change the process exit code.
- dsh-spill-local: revert the exit handler entirely; its default root is
already owned by the package's 30-day startup sweep.
- dsh-subprocess-local/spawn: remove the per-process spill dir at a
JavaScript-observable exit only while it is EMPTY (collectors unlink
their spill files on dispose), best-effort with a named swallow so a
Windows-held handle never changes the exit code; dirs holding spill
files keep them for external cleanup.
- Document the empty-dir exit removal in the package README and the Agent
Note, and re-record both bilingual pairs.
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()
The exit listener runs after the coverage dump, so its body can never be
measured by the unit coverage lane; mark it v8-ignore with the reason, as
with other process-exit-only code paths.
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.
Full-template residue histogram on the CI host surfaced four more normal-exit
leaks below the earlier cutoff: tool-bash/tool-pwsh tools.spec (module spill
dir / per-test homes), bash-sandbox sandbox.spec (module spill dir and the
read-only denial root), and fs-sandbox fs-sandbox.spec (inline tmp dir). All
four now remove what they create at the same teardown points as their
neighbors.
Spec files that create /tmp/dsh-* directories via mkdtemp now track and
delete them in afterEach/afterAll; module-scope fixture dirs (executor
spill dirs) are removed in afterAll. The file list came from the
observed-residue inventory on the self-hosted CI host: only specs whose
dirs actually accumulated were leak sources (issue #3134), superseding
the kept-but-unmerged CI sweep branch per the #3233 review decision.
Product per-process spill roots (dsh-subprocess-local spawn,
dsh-spill-local store) register a process-exit handler that removes the
memoized dir, so processes that used the spawn/spill path clean up on
normal exit. A SIGKILLed process cannot run in-process teardown; the
machine-side timer remains the backstop for that path.
Agent Note: .agents/notes/implemented/process/2026-08-28-test-temp-dir-self-cleanup.md
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.
Expose a live goals/get read plus goal/activation-changed for the Web strip, reject model resume of durable paused goals, refresh snapshot and generated documentation surfaces, and record the follow-up decision.
- `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 feedback: the dsh-terminal-bash product default is 30s, not 300s;
the raised value bounds one send plus the complete startup sequence, so it
must cover the same cold start the tool deadline does. The vitest case
budget stays at its pre-existing 120s: the case-level timeout overrides
the lane --testTimeout, so syncing it to the plugin deadline would make a
stalled partition wait 300s instead of 120s.
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.
Replies captured on 2026-09-02 from OpenRouter, models.dev, and DeepSeek
replay through the discovery parser, and OpenRouter's nested
top_provider.max_completion_tokens now feeds the output-token cap.
Model requests hand the configured baseURL to pi-ai unchanged. Only the
discovery listing URL accepts the Anthropic API root with or without a
trailing /v1, because gateway documentation publishes both spellings.
The new keyless sdk-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,
so a pkg upgrade that re-records the child-process patch cannot silently
restore the hijack. Runs on every target through --scenario all.
@yao-pkg/pkg's SEA bootstrap rewrites child_process commands named node --
including the string after a -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 booted the dsh CLI
instead of the machine's Node. Pin the packager as an exact root
devDependency invoked through pnpm exec and patch out the single
patchChildProcess call from the SEA bootstrap bundle; packaged children now
resolve node through PATH like any other process. The third-party notices
drop the build-time tools section: the packager is now a declared, patched
devDependency, so the manifest and patch tiers disclose it.
master released 0.1.2-alpha.5 after this package was created, so the
release bump never reached it; the workspace constraints gate requires
every package version to match the root.
Prefer gh v2.99.0's repeatable --attach flag for publishing demonstration
GIFs: it uploads the artifact and rewrites the body's local-path
reference in place, keeping media out of git history without an orphan
assets branch. The assets-branch workflow remains the fallback when the
GIF exceeds 10 MB, gh is older than v2.99.0, or the repository is not on
github.com.
Review follow-ups: the Agent Note triplet moves to implemented/ rewritten as
shipped state (Decision/Consequences/Testing, present tense), cross-linked
both ways with the 2026-07-28 storage recovery proposal whose projcache
reset/destroy path it supersedes (that proposal stays live for authoritative
and whole-medium damage). The fixtures spec header and the note state the
fixture provenance as recorded facts of the released builds instead of
citing local tooling, and the spec JSDoc points at the note's final home.
The proposed Agent Note records the three shipped on-disk generations of
session_projcache, the read-compat and backup-and-skip decisions, the
upgrade matrix, and the rejected alternatives. The package README documents
the upgrade guarantees and requires every future schema or domain-version
change to land with archived fixtures and tests proving its upgrade story.
The storage subsystem page and the generated cordis catalog pick up the new
DomainSpec fields.
The session_projcache domain declares compatibleVersions: [3, 4] and
invalidRecords: 'backup-and-skip'. The two lineage identity fields become
optional — records admitted from older versions predate them, and the single
reader (identityMatches) interprets absence as the unseeded lineage: exact
for unseeded sessions, while a seeded caller fails the match and refolds
cold, so the lineage binding keeps its protection. Upgraded homes therefore
boot and serve their cached listing titles immediately, including homes
whose new tree already holds current-stamped documents without lineage
fields, and a record failing validation anyway is backed up and skipped
instead of refusing the plugin tree.
tests/fixtures/ archives the real on-disk media of every shipped generation
(v3 whole-unit file, v4 and v5 per-record documents, and the lineage-less
current-stamped shape); fixtures.spec.ts proves each recovers through the
real storage stack, rewrites to the current format on the next live write,
and that a hopeless record is salvaged without costing the boot.
A DomainSpec may declare compatibleVersions: older domain versions whose
stored records the current record schemas still accept. The json backend's
per-record reads admit documents stamped with a declared version (writes
always stamp the current one), and the legacy whole-unit bootstrap migrates
only a file whose stored version is in the accepted set — previously it
migrated any version and stamped the records current, turning a discardable
stale cache into invalid-record failures that refused the whole domain at
open and permanently poisoned the new tree on first boot.
A DomainSpec may also declare invalidRecords: 'backup-and-skip' for domains
whose records are disposable derived data: a stored record failing its zod
schema is moved aside through the new optional KvUnit.backupRecord
(<key>.json.bak.<YYYYMMDDHHmm> under the json backend), logged with its
cause, and skipped, instead of rejecting the open. The default stays
fail-loud, and so do backends without backupRecord.
The first call pays the full pwsh cold-start latency (spawn + .NET +
PSReadLine + Defender) inside the tool deadline. A 60s bound on the fully
loaded self-hosted Windows pool is exceeded often enough to reset the
session mid-test: two master CI runs (2026-09-01, runs 33524764567 and
33534262413) each failed at ~62s with the second call observing a fresh
session (cwd back at root, env empty). 300s matches the product default
so cold start no longer races the budget.
Windows folds `https_proxy` and `HTTPS_PROXY` into one variable, so the
exported spelling shadowed the file's lowercase one and the case failed
there. Each spelling now gets its own name: the file supplies a name the
shell did not export, and the shell outranks the file for the one it did.
The 0.1.2-alpha.4 release on master touched every manifest that existed
there; this package did not, so the merge left it at alpha.3 and
`check-workspace-constraints` refused the tree.
Refusing a response whose tool call never receives an identity needed a new
failure code, a change to the default retryable set, and a `[DONE]` gate that
overrode the finish reason a provider had already sent — turning a safe
`max-tokens` truncation into up to five retries. The lenient wire it guarded
against is hypothetical: no report describes a stream that omits identity
entirely, and the pre-existing test for it is labelled as such.
Only `acceptIdentity` and the widened wire types remain. They close the
reported erasure and cannot reach a worse outcome than the previous
assignment, because the set of inputs that assign only narrows.
`proxyEnvironmentForChild()` hands a child the proxy values the user
exported, including one this package refused — a SOCKS URL kept because
`curl` reads it — and sets `NODE_USE_ENV_PROXY=1` so a child Node honors
them. Node parses `HTTP_PROXY` and `HTTPS_PROXY` under that flag before
running the program and exits on any scheme other than `http:` or
`https:`. So a user with a usable `HTTP_PROXY` and `HTTPS_PROXY=socks4://…`
lost every Node child — stdio MCP servers, subagent CLIs, `npm` in the
bash tool — before its first line, while this process had reported only
that the scheme stayed direct. Measured on Node 24.17: `socks4://`,
`ftp://`, and a malformed value all exit 1; `socks5://` is accepted there
and only there.
The flag is now withheld whenever a value under the names Node parses is
one `isSupportedProxyUrl` refuses. Such a child connects directly, which
is what this process already said about that scheme, and `curl` still
reads the value it was kept for. Node does not read `ALL_PROXY`, so a
refused value there alone changes nothing.
The socks5 case in `install.spec.ts` now asserts the flag absent; the
ALL_PROXY fill case asserts it present; a new case spawns a real child
Node under the overlay for each refused shape and asserts it starts.
A direct policy installed over a proxied one swapped the global
dispatcher but left `process.env` holding the outer install's published
normalization. `proxyEnvironmentForChild()` returns nothing under a
direct policy, and `scrubbedParentEnv()` copies `process.env` as it is,
so a child spawned in that window inherited values no active policy
stood behind: an `HTTPS_PROXY` derived from `HTTP_PROXY` the user never
set, or the loss of a SOCKS value they set for `curl` and this package
had refused.
The direct branch now writes the user's own values — the record the
outermost install keeps — back into `process.env` for the window, and
re-applies the outer install's published values when it ends. An
install underneath that proxied nothing published nothing, so there is
nothing to put back. Publishing and restoring share one `writeProxyEnv`;
the empty-string special case it replaced was unreachable, since a
resolved bypass list always carries the loopback entries and an
accepted proxy URL is never empty.
The nesting is reachable only from tests since the plugin was removed;
the fix keeps the disposer symmetric for whoever layers installs next.
`LlmRuntime.adapterStream` normalizes a thrown `LlmError` into the same error
`finish` the loop routes to `agent/request-error`, so throwing would reach
retry too. The Note claimed otherwise. Yielding is chosen because it reports
the attempt's billed usage first and matches the neighbouring `EMPTY_RESPONSE`
refusal.
The rejection comment repeats the corrected durability wording, and the
assembler's delta-only fallback carries a TODO for the empty name it still
invents for adapters that never close a tool-call block.
The proxy guide told users a proxy could live in a project or
`$DSH_HOME` `.env`. It could not: `loadLayeredEnv` refuses the four
proxy names from any discovered file, as it refuses `PATH` and
`NODE_OPTIONS`, and the launch fails with a pointer to `export`.
That refusal is right for the invoking directory's file — it arrives
with a clone, and a repository must not choose where the harness sends
its traffic — and wrong for the user's own `$DSH_HOME/.env`, which
already holds their API key. `readEnvLayer` now accepts `HTTP_PROXY`,
`HTTPS_PROXY`, `ALL_PROXY`, and `NO_PROXY` from the directory that is
the Harness home, and nowhere else. `DSH_HOME` is itself bootstrap-only,
so no `.env` can relocate the exemption; the CA and TLS names in the
same group stay refused everywhere, since they change what is trusted
rather than where traffic goes. A project `.env` that sets a proxy name
still fails the launch, and its message now names the home file as the
second way out. Launching from inside the home directory reads that one
file as the project layer; the exemption follows the directory.
The seven existing refusal cases all write to the project layer and
pass unchanged. Four new cases cover the home layer accepting both
casings below an exported value, the home layer still refusing
`SSL_CERT_FILE`, the project layer's new message, and the same-directory
launch. The guide, both package READMEs, and the two Agent Notes that
stated the old rule now state this one.
The `MALFORMED_TOOL_CALL` JSDoc claimed nothing durable is written, but the
loop appends an `assistant/chunk` for every yielded chunk; only the assistant
message and tool result are withheld. The bounded-recovery Note still listed
a five-code transient set, and neither Note linked the other.
A keyless `malformed-tool-call-retry` scenario now records the refusal, the
retry, and the absence of a `tool/call` for the failed attempt. A translator
case pins that an already closable block also withholds its `block-end`.
Review found the package's prose still describing a plugin that an
earlier revision removed, and three factual slips about behavior.
- policy.ts, install.ts, install.spec.ts, and the Agent Note named a
`Config` surface, a `cordis.yml` source, a mountable plugin, and a
`plugin.spec.ts` that no longer exist; each now describes the
environment-only resolution the launcher actually runs.
- `NO_PROXY=example.com` bypasses `api.example.com` as well — the matcher
accepts the host and every subdomain under it, and a leading `.` or
`*.` means the same thing. The guide, README, and JSDoc claimed a bare
entry matched only the exact host, which would let a reader believe a
subdomain was proxied when it went direct.
- A rejection diagnostic names the variable and never its value, so no
username is shown; the guide said the username was shown with the rest
masked. The README's source map claimed a "redaction" step that does
not exist.
- The `node:https` worker placeholder was added for a `node:http` agent
factory this PR later removed; nothing imports `node:https` now, so the
stub, its VFS mapping, and its test return to their state on master.
Adding `MALFORMED_TOOL_CALL` to the default retryable set changes two
expected outputs the session snapshot lane does not own: the shipped Web
composition's inline snapshot, and the canonical packed layout of the
refreshed `empty-response-retry` fixture.
The persistence seam is now create/open/stat/list returning per-session
SessionHandles (read/append/flush/close); every log read and write flows
through the owning handle. The seam package exports only the service and
handle contracts, consumer-visible errors, and pure durable-data
validation helpers; each backend owns its complete storage runtime, and
the shared contract suites pin equivalent observable behavior. The
backend routes published sessions' live events by id into the active
write handle; agent-loop only acquires, seeds, and closes the handle.
Resume appends interruptedTurnClosers through its write handle;
session-query owns the revision-keyed cold cache. Legacy-only surfaces
are removed in the same swap: locate/readRaw/supportsRawArtifacts, the
legacy event-shape read migration, zstd torn-frame salvage,
DSH_SESSION_JSONL, and hook transcript_path population; a torn final
zstd frame is discarded whole; the session-list cold blank probe returns
on stat metadata (eventCount derived from the last physical row,
sizeBytes). The WebUI ZIP export serializes the logical log from a read
handle, so both backends export identically.
Refs #3245
A continuation SSE delta that repeats a tool call's `id` or `name` as an
empty string — or as `null`, which some OpenAI-compatible gateways send —
erased the identity established by the call's first delta. The assembled
block reached the loop with an empty name and failed as `unknown tool ""`,
and the empty `callId` persisted into `tool/result`, which the session
reader refuses on reopen.
`acceptIdentity` accepts only a non-empty string, so a repeated empty or
null field means "no update". A tool call still missing `id` or `name` at
`[DONE]` ends the response with the new retryable `MALFORMED_TOOL_CALL`
code instead of closing an unusable block.
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.
Reverting telemetry to its own transport dropped the package's dependency
on the proxy library, and the generated graph still recorded that edge.
The gate that catches this is `verify-module-graph`, which runs in
`check:ci:static` and not in `doc-sync`; the revert was checked with the
latter alone.
Telemetry was the only call site this PR could not cover without changing
the SDK transport underneath it, and both ways of doing that cost more
than the channel is worth.
Routing an `http.Agent` needs Node's `proxyEnv`, added in 22.21 and 24.5
— inside the engines range, so three supported runtimes stayed direct
anyway, and the proxy package had to keep a `createNodeHttpAgent` export
for a path that only sometimes worked. Replacing the transport with the
SDK's `fetch` delegate covered every runtime but has no compression,
while the shipped `base` bundle enables gzip and a realistic OTLP batch
is 6.4x smaller with it; keeping both meant gzipping at the serializer,
which put transport code inside a telemetry plugin.
Telemetry is the one outbound channel whose loss costs the user nothing:
no tool, model request, or session depends on it, and an export that
cannot connect is already dropped silently. A user behind a mandatory
proxy is left where they were rather than regressed.
`src/index.ts`, `otel.spec.ts`, and `tsconfig.json` return to their state
on master; the package keeps only a dev dependency on the proxy library.
`egress.spec.ts` inverts: it installs a policy and asserts the fake proxy
saw nothing, so an SDK upgrade that moved the exporter onto `fetch` would
surface as a failing test rather than silently routing telemetry.
The previous commit moved the OTLP exporter to the SDK's `fetch` delegate
and refused `exporter.compression` at load, on the belief that nothing
shipped enabled it. `packages/bundle/base/cordis.patch.yml` does, so the
refusal broke every test that boots the shipped bundle — the snapshot,
e2e, and Windows observational jobs all failed on that one load error.
Dropping gzip was the wrong trade anyway: a realistic OTLP batch measures
6.4x smaller with it, so trading it for proxy support would have charged
every deployment to fix one. The `fetch` transport has no compression
hook, but serialization is the seam before the body reaches it — the
plugin now gzips there and declares `Content-Encoding` itself.
`keepAlive` and `httpAgentOptions` have no such seam, since they
configure a connection pool `fetch` does not expose, so those two stay
refused at load rather than accepted and ignored. `compression` is typed
as the two values this package can actually apply rather than the SDK's
wider enum, and a third value fails loud at load.
The code-mode → ptc rename rewrote Cloudflare's product name in the
link text and the external URL path, leaving "PTC mode" pointing at
https://blog.cloudflare.com/ptc/ (404). Restore Cloudflare's own name
and the working https://blog.cloudflare.com/code-mode/ link in both
the English and Chinese notes.
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 package exported six functions, four of them shaped by one SDK's
transport each: a dispatcher factory, a `node:http` agent factory, a
proxy-URL lookup, and a policy accessor. Review asked whether the call
sites could converge instead of the package growing an export per SDK.
They could, and each removal took a whole shape with it:
- The OTLP exporter moves to the SDK's `fetch` delegate, retiring
`createNodeHttpAgent`. Its Node-version floor goes too: `proxyEnv` on
an `http.Agent` needs 22.21 or 24.5, inside the engines range, so
telemetry was direct on 22.19, 22.20, and 24.0-24.4. The cost is
`compression`, a Node-transport option; the plugin now refuses it,
`keepAlive`, and `httpAgentOptions` at load instead of ignoring them.
- `web-fetch-http` builds its own address-pinning agent under an
annotated `proxy-exempt:` exemption, retiring `createDispatcher`.
Pinning is per-request state a process-wide dispatcher cannot hold.
- E2B reads `route.proxy`, retiring `proxyUrlFor`.
What remains is `installProxyFromEnvironment`, `proxyRouteFor`,
`proxyEnvironmentForChild`, and `clearedProxyEnv` — one per way a caller
can need the policy. Installation absorbs resolution and diagnostic
reporting, which no caller needed apart.
`proxyRouteFor` also closes a defect the old accessor made expressible:
`web-fetch-http` read the policy to decide whether to pin, then read it
again to build a transport, so an unmount between the two returned a
direct, unpinned agent for a URL the first read had cleared as proxied.
A route carries the answer and the transport that answer assumed.
Every egress spec now installs through `installProxyFromEnvironment`, so
no test asserts a policy object a real launch could not produce.
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.
The two styling notes name the surveyed product; the mechanism facts
(superellipse token, guard, full-round opt-out, stroke-in-shadow
elevation, 0.5px hairline) stand alone, so the notes now state them
without the product reference. Pairing records re-recorded.
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.
The composer hairline moves one step up from the menus' l1; the seven
menu-fill dropdown cards grow from 16px to 20px corners. Notes and the
token comment record the new layering.
Re-declare the derived elevation tokens on body * so per-surface
--dsw-elevation-stroke-color rebinds reach the consuming shadow (custom
properties inherit with var() already substituted); pin that mechanism
and add synthetic rejection cases to the stylesheet scans; take the
ring-track basenames through node:path so the exemption matches on
Windows; update the ModelsSection row-card spec to the hairline recipe;
align the elevation note with the shipped l1 menu rebind, refresh the
feedback-popover note's surface recipe, and document the soft tier.
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.
Apply global visual polish across the web client: corner-shape:
superellipse(1.5) with corner-shape: round pairing for full circles,
elevation tokens that draw 0.5px stroke outlines inside box-shadow for
floating surfaces, 0.5px hairline borders and divider lines for
neutral-token strokes, and tuned stroke contrast plus larger radii for
menus, settings panels, and cards. Stylesheet-scan specs in ui-theme
reject unpaired circles, border+shadow mixes, and 1px neutral hairlines
repo-wide.
Closes#3287
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.
Master moved 212 commits. Conflicts were eight `package.json` and nine
`tsconfig.json` files, all dependency-and-reference unions: master dropped the
`runtime-diagnostics/invariants` reference across packages while this branch
added `util/http-proxy`. The lockfile, `tsconfig.base.json`, and the module
graph were regenerated rather than merged by hand.
Master also brought `verify-package-invariants`, which rejects an empty
invariant companion. This package's companion was empty by design, so it is
gone with its publication wiring — the export, the `files` entry, the project
reference, the `dsh-invariants` peer, and the test. Master did the same across
`util/`. The README carries the reason sentence that gate requires.
Carried in the same commit, because the review arrived while the merge was open:
`e2bApiUrl` moves out of the `dsh-e2b` entry into `src/api-url.ts`. Code Entropy
flagged it as a public name with no consumer outside its own package; the seam
keeps its URL-precedence test, which is worth having directly — getting that
order wrong hands a proxy the control-plane traffic and its API key.
Review asked why this is a plugin and why it exports so much. The design note
this branch shipped answered the second question itself — "a pure resolution
function plus an installation function" — and the code drifted to seventeen
exports and a Cordis plugin nobody approved or mounted.
The plugin is gone. Transport policy has one answer per process: nothing to
swap, and no scope narrower than the process to give one. Its `Config` was also
the only supplier of a configuration branch, so resolution now reads the
environment and nothing else — `mode`, the config-sourced fields, and the
`config` policy source were unreachable the moment the plugin left.
Four exports nothing outside the package used are internal again, and
`currentProxyPolicy` answers with the direct policy instead of `undefined`, so
`DIRECT_POLICY` no longer needs a public face. Nine functions remain, one per
way a caller can need the policy; two is not reachable with six consumer seams.
The package moves to `util/`. The note claimed a dependency on `undici`
disqualified it from that group; the charter governs harness dependencies, not
external ones, and the process note that says so predates this branch. The real
blocker was the harness dependency: resolution needed one method of
`LaunchEnvironmentSnapshot`, so it names a structural `EnvLookup` and the
launcher passes its snapshot unchanged. `net/` is dissolved.
Dropping the group's line from the repository layout also returns `AGENTS.md`
to its original ceiling, so the raise the merge needed is reverted.
Review follow-up: the per-step gate recomputed view(previous) on every
changed apply — a property read for identity-stable views, but a fresh
throwaway object per change for computing views. The registry now keeps a
WeakMap from state object to raw view: the previous state's view was
cached when that state was current, so each distinct state's view computes
exactly once (gate and snapshot share the memo) and the quiet path
allocates nothing. Unlike the earlier lastView record, an entry is keyed
by the state itself — the view of that exact state by the pure-view
contract — so no stamping discipline exists to get wrong. Primitive
states bypass the WeakMap and compute directly.
Review consensus: for the only declaring unit the token function was
literally the view function, so the extra wire member bought nothing —
default raw-view comparison produces the identical Object.is on
state.turns at the identical cost (one property read per side). The
per-step gate stays: it holds the no-stored-baseline property, and its
measured overhead (~ns per changed state) is four orders of magnitude
below the push path it guards.
Review follow-up (imccyu): comparing view(previous) recomputes the view on
every quiet change. The wire block now takes an optional viewKey(state)
declaring the cheap comparison token; the drive compares tokens across the
previous and next states and calls view only for an actual push. Default
stays the raw view output. turnOutline declares viewKey: state => state.turns,
making the identity-stable-turns convention an explicit contract; the
registration erasure forwards viewKey (dropping it silently reverted the
gate to the fallback, caught by the new view-call-counting test).
Review suggestion (imccyu): the drive holds both the previous and next
state, so the identity gate can compute view(previous) and view(next) in
the driving step and compare them directly. The stored lastView cell field
and its stamp-on-every-change rule are deleted; with no dedup memory,
nothing can go stale across listener generations by construction, and a
rebuilt cell no longer pushes an unchanged view on its first live event.
Costs one extra pure view() call per changed state.
The dsh 0.1.2-alpha.2 release bumped every workspace after this branch
forked, so the new package failed the workspace version constraint in the
merge tree. The bounded preview() also grew identical enough to its
deliberate host/client mirror to trip clone detection; the client copy now
carries a jscpd ignore region naming the wire-boundary rationale.
preview() now slices a single text block to limit * 2 before joining and
normalizing, so one multi-megabyte block no longer pays a full-string pass;
the host projection and the client turn-navigation helper stay mirrored.
outlineEntry keeps dropping entries with damaged turn/seq (marks cannot
exist or jump without them) but degrades malformed prompt/response
previews to empty strings so the turn stays navigable.
The change feed stamped lastView only when a listener was subscribed, so a
value change during a listener-free window (HMR swap) froze the baseline
and a later transition back to the old value was silently deduplicated.
The baseline now advances on every changed state, heard or not; broadcast
still only happens with listeners. Docs, catalog, and the feature note
follow the corrected semantics.
A jump clicked while the Load-earlier pull was in flight fell through the
settle effect's nearest-turn fallback and landed on the wrong row. The
effect now keeps the pending jump (busy pulse stays) while loadingOlder is
true and a retry tick re-issues loadThrough when the pull settles.
Also repins the long-interactions e2e rail block to the outline-rail
semantics (fixed mark pitch, load-and-jump labels) and covers the
loadThrough forwarding paths in apply-inject and the client-runtime stub.
loadThrough now assigns its low-water target only when it owns the loop
(retargeting stays inside the running-jump branch): a call refused while
a plain load-earlier pull holds the pager no longer leaves jumpTargetSeq
behind to drag a later jump all the way to the head. The loop also
carries the doOpen stale-pass guard so a mid-flight resync stops it
instead of paging the new stream generation toward the old target.
Edge-branch coverage for the preview reading bound, repeated and empty
drafts, draftless turn ends, orphan-draft clearing, and same-response
recommits (the CI per-file gate exercises them); the jump-settle spec
now asserts only the busy lifecycle after settlement — jsdom's zero
geometry made the rAF active-turn resync timing-dependent under
coverage instrumentation, and the landing position contract lives in
the browser e2e.
Outline entries gain the turn's final text-bearing assistant preview:
each assistant message overwrites a state draft and turn/end commits
the survivor, matching the loaded rail's findLast semantic; the bare-
array wire keeps its identity across draft changes, so pushes stay at
three per turn. Preview budgets shrink to the rail card's clamps — one
50-character prompt line, up to three 120-character response lines,
ellipsis on clip — on loaded and unloaded turns alike (stateVersion 2
discards v1 cache rows).
The feed previously fired on every changed state reference of a
client-visible unit; it now also compares the raw view output against
the last delivered one and stays quiet when Object.is-identical, so a
unit can buffer working fields in state behind an identity-stable
projection. Units whose views build fresh objects per call are
unaffected.
Clicking an unloaded rail mark from the pinned tail raced the pinned
scroll snap: the first prepend's compensation fires a non-reader scroll
delivery, the snap called toBottom, and toBottom cancels a pending jump
— so the jump silently stayed at the tail while history loaded. The
click now drops atBottom itself (jumping into history is leaving the
live tail), pinned by a jsdom regression and re-verified live: a
118-turn session lands on turn 1 in ~250ms from click.
READMEs record the rail's outline merge and the loadThrough paging
verb in both languages; the feature Agent Note owns the decision,
alternatives (sparse windows, minSeq wire bound, outline RPC, height
estimation), and coverage map. Regenerates the client/API catalogs the
widened faces feed.
A mid-paging landing keeps the jump armed with the target row as its
paging anchor, so later chunks and the load-earlier button's unmount
cannot drift the landing; the loader's completion runs one final
correction unless the reader already scrolled off the target. Adds the
browser contract: full outline ladder, keyboard jump on an unloaded
mark, landing geometry, and rail fades.
Marks keep a fixed 10px pitch instead of compressing into the frame:
overflow scrolls inside a hidden-scrollbar scroller with gradient fades
over each still-scrollable end, the preview compensates the rail scroll,
and the active mark keeps itself centred while the pointer is off the
rail. Pointer-to-mark mapping now works in ladder coordinates.
The rail now merges the turnOutline projection with loaded-window items
(view-layer only; loaded wins, outline fills mid-turn prompt previews),
renders unloaded turns as dimmer marks, and clicking one holds the
reader's place, pages history through the turn's seq, and lands on its
row after the commit — no height estimation. Settlement repages once
per head movement, then falls back to the nearest rendered turn.
Session.loadThrough(seq) loops the existing prepend pager (200-message
pages) until the window covers the target seq, with a shared low-water
retarget for repeated calls, a no-progress guard against empty pages
still claiming history, and loadOlder's fail-soft error posture. Busy
state rides the existing loadingOlder snapshot bit.
New turnOutline projection unit serving every started turn's number,
turn/start seq, and bounded first-prompt preview through the
session-projection seam, mounted in the web-app bundle for the chat
turn rail. Entries stay strictly increasing; previews mirror the rail's
loaded-turn preview budget.
The hostFrameParseCeiling JSDoc and one load-gate test comment still quoted
the pre-16x derivation (~29 MiB for a ~300 MiB heap, ~14 MiB for a 128 MiB
old space). With HOST_PARSE_WORST_CASE_MULTIPLE = 16 the same hosts derive
~14 MiB and ~7 MiB (floor((176-64)/16)); protocol.spec.ts pins the 304 MiB
case at 15 MiB. Comment-only correction, no behavior change.
The #3289 merge-forward dropped four typescript/no-unnecessary-condition
suppressions from index.ts (boot-write-failure fake child stdin, the
admit()-closure logsTruncated recheck, and both settled rechecks whose
guards flip mid-wait), turning the lint:contracts-ready gate red. Re-add
them with their reasons. The merge also carried a ptc-python-turn session
fixture that was not in canonical packed layout; migrate-packed-session-fixtures
re-writes it so session-fixture-layout passes.
render() loaded the workspace manifests once per external dependency
name through workspaceLinkedManifest, an O(names x manifests) file
read on the cold path; on the loaded self-hosted Windows host with
coverage instrumentation the freshness spec crossed Vitest's default
5000ms budget and failed the serial-windows standby gate four times in
a week. Load the manifests once in render() and thread the map through
the collectors instead.
The serial-windows lane also ran the coverage inventory at the strictest
budget of any lane: add DSH_COVERAGE_TEST_TIMEOUT_MS=90000 to match the
pull-request windows-coverage lane, pinned by ci-workflow.spec.ts.
Review findings on the CPython backend: a child that never reads fd 3 leaves
the reply pipe full forever, so the drain loop waits on 'drain' while every
call frame it keeps sending resolves a binding and queues another reply —
the backlog (and the binding results it pins) would grow until the wall
clock. sendReply now caps the pending backlog at MAX_PENDING_REPLIES and
settles the run as worker-exit past it, mirroring the frame cap; a child
flooding calls against a binding that never settles would otherwise bypass
that cap (pendingReplies grows only after the await), so the dispatcher
counts in-flight binding calls before dispatch and releases the slot in the
async body's finally, capping outstanding closures at the same bound. The
drain also compacts its consumed prefix (replyQueue.splice(0, head)) once
head reaches the bound, so a drain that stays alive without emptying cannot
grow the backing store linearly with cumulative throughput.
The completion-value meter counted lone surrogates with
_SURROGATE.findall(folded), materializing one single-character string per
surrogate: a surrogate-dense value near the budget (millions of surrogates,
each serializing to six bytes) allocated millions of objects before the meter
returned, defeating the meter's counting-without-building contract. The count
is now the length difference between folded and the without string the meter
already computes; a standalone equivalence check confirms it matches findall
across lone-high, lone-low, paired, astral, and mixed cases.
validateBindings read namespace.global/errorClass.name/memberNameProperty
several times and retained the original errorClass object for the boot
frame, whose JSON.stringify re-read it after validation: a stateful getter
could throw or change between the two stages, turning the seam-misuse
rejection into a worker-exit or injecting an unvalidated name. Each field is
now read once into a plain value and the bindings map stores a plain
{ name, memberNameProperty } copy, so validation and the boot frame see
identical values.
Regression tests: a hostile child floods 5000 sequential valid calls without
reading fd 3 and the run settles worker-exit with the reply-queue message
before maxWallMs; a 3,000,000-surrogate completion succeeds at an
18,000,002-byte budget and reports output-limit one byte under; a 5000-call
flood against a never-settling binding settles worker-exit with the
call-backlog message; getter-backed namespace metadata that throws or
changes on a second read boots and runs with each field read exactly once; a
two-wave flood whose replies exceed the writable high-water mark drives the
drain past the compaction bound mid-delivery and verifies all 1524 replies
arrive. README Known Limitations gains the reply-backlog and call-backlog
bounds (en/zh, pairing re-recorded); a new Agent Note registers the findings.
The capability-seams graph derives its implementation lists from
SERVICE_ROLES in scripts/gen-doc-graphs.ts, which still listed only the
worker-thread backend. Add experimental-code-runtime-python so the
generated graph and table match the registered ctx.codeRuntime
implementations; regenerate docs/capability-seams.md, sync the zh pair,
and re-record the i18n pairing.
Review findings on the CPython backend: an explicit pythonBin path bypassed
the load-time checks (missing/non-executable/directory paths surfaced only
as a run-time worker-exit); a throwing binding member accessor escaped the
fd-3 data callback and terminated the host; the reply drain waited on
'drain' alone, so a pipe destroyed under the wait hung forever; and two
staging-leak assertions diffed a global tmpdir that parallel workers can
perturb.
resolvePythonBin now applies the same accessSync(X_OK) + isFile check to
explicit paths (resolved against the host CWD), and the load error message
distinguishes 'is not an executable regular file' from 'does not resolve on
PATH'. validateBindings snapshots callables into a plain record during run()'s
synchronous validation, turning an accessor throw into the seam-misuse
rejection and fixing the key set the boot frame and dispatch share. The reply
drain waits on drain/close/error together and short-circuits on
proto.destroyed. The staging-leak assertions check the exact paths this test
file staged (recorded by the mocked mkdtempSync) instead of a tmpdir diff.
docs(code-runtime-python): add the alternatives section to the hardening note
docs(config-catalog): refresh the code-runtime-python Config source line
test(code-runtime-python): cover the async spawn-error worker-exit path
The review's items: the experimental group README's Packages table and Summary
now list code-runtime-python (CPython subprocess backend, ctx.codeRuntime),
paired; the portable-identifier note's Scope drops the 'has since shipped …
now lists' change narration in favor of the current state, removing the
apparent contradiction with 'the worker is the only shipped backend'.
The review's final wording items: the portable-identifier note's Scope said the
backend 'has since shipped' without noting it is experimental/private; the
RESERVED_WORDS JSDoc said backends 'ship for both languages'. Both now name the
TypeScript backend as released and the CPython backend as experimental and
private. The package README also records that the truncation-marker text and
tempdir prefix keep the pre-rename short names (byte-anchored by tests,
independent of the npm name).
The review's carry-over: 'each has a published backend' in the CodeRuntime
JSDoc and its projections (tool-cordis api-catalog, subsystems page) plus
'both shipped'/'backends ship' in the code-runtime README all claimed the
Python backend is released; it is private and experimental, excluded from the
release family. The wording now states the TypeScript backend is released and
the Python backend is experimental and private (not published), in the JSDoc
(api-catalog regenerated to match), the READMEs (paired), and the subsystems
page (paired).
The fd-3 note's Status line moved off line 3 when the experimental-location
fact was added; it is back as the sole line-3 status. The zh portable-identifier
note's merged Scope paragraph lost its blank-line separator, which the
md-wrap gate read as one hard-wrapped paragraph — the blank line is restored.
The review's move-follow-ups: the Windows test exclude now points at
packages/experimental/code-runtime-python (the constructor throws by design on
Windows, so the suite must stay excluded); the invariant companion and
@module annotations use the new npm name; the truncation marker text and
tmpdir prefix stay as-is (tests anchor them); the package JSDoc and READMEs no
longer call the private experimental backend 'published'/'shipped'; the
code-runtime README row describes the package as protocol AND runtime; the
fd-3 note records the package's experimental location.
The move broke two generated/derived surfaces: (1) the package invariant
companion still registered the old name
@deepseek-ai/dsh-code-runtime-python, so the exhaustive-topology test found
the new name unreserved — it now registers
@deepseek-ai/dsh-experimental-code-runtime-python; (2) the tsconfig.base.json
alias for the renamed package sat inside the generated region, so
gen-tsconfig-paths dropped it (a package named after something other than its
directory needs a hand-written alias before the BEGIN marker) — the alias is
moved out and the config is current again.
The CPython code runtime's complete public contract is experimental, so it
moves to packages/experimental per the experimental-packages rules: npm name
@deepseek-ai/dsh-experimental-code-runtime-python, private: true, no
publishConfig. All references updated (code-runtime READMEs, config-catalog
and module-graph regenerated with zh alignment, tsconfig paths, doc-standard
and workspace-constraints scripts, the fd-3 and settlement Agent Notes, and
the package README links); md-links and translation pairing pass, and the
suite still runs green.
The review's item: the Problem section counts eleven no-fail-before fixes, but
the Consequences section still said 'the ten called out in the Problem section'
(zh: '那十处') and omitted the new unknown-binding preview cap. Both sides now
say eleven and name the cap, paired and re-recorded.
The review's warning: the settlement note's Problem paragraph says ten fixes
have no fail-before test, but the unknown-binding preview cap (a transient
whole-target JSON.stringify peak, unmeasurable through the seam) is the
eleventh. The count and the item are now recorded, paired.
The review's suggestion: the settlement note enumerates each review fix in this
PR, so the unknown-binding preview cap (escaped from a 1 KiB prefix,
capMessage still enforces the reply budget) gets its own short section, paired
and re-recorded.
The reviewer's standing item: the unknown-binding reply ran JSON.stringify on
the WHOLE capped target (global + '.' + name, each up to maxValueBytes code
units), allocating the escaped form — up to ~6x under control-heavy input, a
multi-hundred-MB spike near the maxValueBytes ceiling that no hostile-peer
bound would have admitted. The escaped preview is now built from a 1 KiB
prefix of the target (enough to identify the binding); capMessage still
enforces the reply budget. A forged huge-name case drives the path.
The review's wording item: the layer-5 bullet ended with 'not by this PR'
(zh: 'not borne by this PR'), which references PR context in durable prose.
The sentence now ends with the current-state fact ('not by this package's
suite'), paired and re-recorded.
The review's open suggestion: the Known Limitations now records that the
real-Loader assembly snapshot is deferred to issue #1182 layer 5 (this package
is exercised through ctx.plugin and real-subprocess tests; the full application
composition is covered by a tracked assembly test in that layer), paired.
The review's two remaining non-blocking items: (1) a case where the run ends
with a SEALED open hold (past MAX_PENDING_CHUNKS) — finish() must commit the
sealed prefix, verified to fail if finish drops openSealed. (2) openSealed is
now a block ARRAY (one joined block per seal) matching the fd-3 reader's
blocks and the stray capture's seal, instead of one repeated string concat
that leaned on V8 ConsString amortization.
The review's follow-ups on the open-seal fix: (1) the settlement note's seal
section now records the HOST-side open hold seal (openParts -> openSealed,
mirroring the child _LogStream and stray-capture seals), paired. (2) the
first-fragment guard comment notes the empty-first-frame case (bills cost + 1 =
3, establishes no hold, bounded over-charge in the safe direction). (3) a
regression case commits a SEALED open hold before the truncation marker —
verified to fail if truncateLogs drops openSealed.
The review's warning: each held open fragment is a distinct array slot plus
string object header (~30x overhead the byte cap cannot see), and a
budget-sized single-character open flood is honest-child reachable
(print('x', end='', flush=True) in a loop). With maxLogBytes near its ~67 MB
load ceiling that was up to ~2 GB of host auxiliary heap. The hold now seals
into one block past MAX_PENDING_CHUNKS, mirroring the fd-3 reader's blocks and
the stray capture's seal; the merge, truncateLogs, and the finish residual all
read sealed + current fragments, and a within-budget flood regression asserts
the merged entry is byte-identical.
The review's wording items: the load-check comment still referenced the
resolvePythonBin JSDoc's old ENOENT promise; the spawn-site comment called the
type assertion a non-null assertion; and two comments claimed the
'logs serialize to maxLogBytes + marker + envelope' bound is recorded in the
README's Known Limitations, which has no such entry — the cross-references are
dropped, the bound stays stated inline.
The review's carry-over: detachResidual (a test seam for the settled run's
resource cleanup) is re-exported from the '.' entry but was not in the README's
declared public surface; the list now names it alongside resolvePythonBin and
readProcessStart, paired.
The empty-open continuation skip (5b61a8fe6) made the Known Limitations entry
stale — the held fragment array no longer grows per empty frame — so the entry
is removed on both sides. The public-surface list now declares
resolvePythonBin and readProcessStart, which the '.' entry re-exports for the
test suite.
The skip branch (an empty open continuation is not pushed into the hold) needs
coverage; a case drives an empty continuation between a first fragment and the
closing frame and asserts the merged entry is unchanged.
The review's items: a zero-content open continuation bills 0 but still pushed
'' into the held fragment array, so a forged empty-open flood grew host memory
without touching the ledger — the push is now skipped (an empty fragment
contributes nothing to the merged entry). The spawn-site comment said a PATH
change between load and run would fail with ENOENT; it actually makes spawn
throw synchronously, which the surrounding try settles as worker-exit.
The review's warning: the pythonBin load-rejection is a product-visible change
(unresolvable basename now fails at load instead of a run-time worker-exit),
but the READMEs (en + zh) only said the basename is resolved against PATH, and
the load-check comment still described the old fallback. The README pythonBin
entries and the load-check comment now state the rejection; pairing
re-recorded.
The review's follow-ups: (1) the product-visible change (an unresolvable
basename pythonBin now fails at load instead of a run-time ENOENT worker-exit)
is registered in the settlement note, paired. (2) PYABS falls back to the bare
name when python3 is not resolvable, instead of interpolating the literal
'undefined' into the wrappers.
The review's follow-ups on the pythonBin change: (1) the JSDoc and the two
call-site comments still described the old fallback-to-bare-name contract;
they now state the load-rejection behavior. (2) the ExceptionGroup case's
version guard raised a skip message on Python < 3.11 but the assertion still
required the truncation marker unconditionally — the assertion now matches
either the truncation marker (3.11+) or the skip message (3.10). (3) the shell
wrappers quote the resolved interpreter path.
The review's two non-blocking items: (1) resolvePythonBin returned the bare
basename when PATH had no hit, and spawn (env:{}) would silently fall to
execvp's platform default PATH and could start a system interpreter the caller
never asked for. It now returns undefined for an unresolvable basename and the
load check rejects it (absolute paths pass through), so the failure is loud at
configuration time instead of silent at spawn; the case that expected a
run-time worker-exit now asserts the load rejection, consistent with the
empty/NUL pythonBin cases. (2) the over-cap exception-group case skipped on
Python < 3.11 (ExceptionGroup is a 3.11+ builtin), matching the TaskGroup
case's version guard.
The review's portability warning: the six shell wrappers exec'd a bare
'python3', which /bin/sh resolves against its compiled-in default PATH while
the runtime spawns with env:{} — in environments where python3 is reachable
only through the caller's PATH (Nix, pyenv) every wrapper run would fail as
worker-exit. The wrappers now bake the resolved absolute interpreter path
(module-level resolvePythonBin, which the product spawn already uses), and
resolvePythonBin is exported for the tests.
The reviewer's residual timing item: the inherited-SIGXCPU reset ran AFTER
setrlimit(RLIMIT_CPU) and the boot-namespace construction, so a huge namespace
under an inherited ignore/block could burn past the soft limit inside that
window and be misclassified as worker-exit. The reset now happens at the very
top of _run, before the resource-limit setup and namespace construction.
The review's two follow-ups on the inherited-SIGXCPU fix: (1) a discriminating
case — pythonBin points at a wrapper that ignores SIGXCPU before exec'ing
python3, so the child genuinely inherits the ignore; with cpuSeconds: 1 the
busy loop must end as timeout (the bootstrap reset restored SIG_DFL), and
reverting the reset leaves it running to the wall — verified red. (2) The zh
README's OUTER wire section now carries the truncation-exception sentence
(the previous commit had duplicated it in the inner section instead); the
duplicate is removed, and the settlement note registers the inherited-SIGXCPU
reset.
The reviewer's standing issue: the child inherits the host's SIGXCPU
disposition and signal mask — if the host ignores or blocks SIGXCPU, the soft
RLIMIT_CPU fires but cannot stop the child, and the hard limit's SIGKILL then
classifies a definite CPU overrun as worker-exit instead of a timeout. The
bootstrap now resets SIGXCPU to SIG_DFL and unblocks it before any model code
runs (the settle-time enforcer already restores SIG_DFL for a program that
traps or masks the signal mid-run; this closes the inherited-state gap). The
zh README's outer wire section also gains the truncation-exception sentence to
match the en side.
The review's warning: the READMEs (outer and inner wire sections, en + zh) and
the fd-3 protocol note still claimed the next log frame always merges into an
open entry, while truncateLogs commits the already-billed prefix as its own
entry before the marker. The one exception (truncation) is now stated in both
READMEs and the owning note, paired and re-recorded. The prefix-commit case's
parenthetical describing the pre-fix implementation is removed per the
comment-does-not-record-review-history rule.
The reviewer's standing issue: line.toString('utf8') silently replaces illegal
bytes with U+FFFD, so a forged frame could land a corrupted completion value
(the honest child's lossless encoder never emits non-UTF-8, so such a frame is
hostile traffic). The fd-3 frame decode now uses a fatal UTF-8 decoder: an
illegal byte throws and the frame is dropped, same treatment as the
unsafe-integer check. A forged illegal-UTF-8 done frame is verified to be
dropped (the run settles on the program's real return), and reverting to
toString makes the case fail.
The review's warning: a flushed unterminated line is billed and committed
(README wire contract says so), but every truncation arm — the child truncated
frame, an over-budget open frame, an over-budget closing frame, and admit's two
budget arms — pushed only the marker, dropping the held prefix: the ledger
charged for output that vanished. All arms now funnel through truncateLogs(),
which pushes the (already billed) held prefix before the marker and clears
openParts, so the prefix survives and only the marker stays last; the finish()
guard drops the now-dead !logsTruncated check (a truncated run has an empty
hold). A regression case asserts [prefix, marker]; the forged-flood and
closing-overflow cases now expect the committed prefix plus the marker.
The review's suggestion: an empty open continuation frame bills zero and holds
one host slot, so a forged empty-open flood grows the held fragment array
without touching logBudget. Accepted as a residual (per-frame host cost far
below its ~30-byte fd-3 wire cost, bounded by pipe throughput, model-code trust
level equal to bash) and now registered in the README's Known Limitations on
both sides, paired and re-recorded.
The review's revision: the buffered-chunks pre-check's open-aware overhead is
observationally inert — when the +3 form trips and the open-aware form does not
(pending + newline in [remaining - 2, remaining]), _push_bounded_prefix
re-slices the same newline-free line text and _push_locked admits it under the
same open-aware billing, byte for byte. The comment now states that the
open-aware form keeps _push_bounded_prefix's 'certain to reject' precondition
true, contrasting with the scan pre-check whose slice carries the newline and
therefore genuinely truncates.
The review's warning: the en README's inner 'Wire contract' section still
described only the truncated flag while the zh counterpart (and the outer 'The
wire' section) described open. The inner en section now matches. The exact-fit
closing-line case comment described the buffered-chunks pre-check recipe while
the program actually drives the scan pre-check; the comment now states the
actual arithmetic and path (and the buffered variant was dropped — its writes
coalesce into one call in the test environment, so it did not discriminate).
The review's warning: while an open entry accumulates, the newline pre-checks in
the write path still charged a NEW entry's +3 cheap-bound overhead (quotes +
separator), so an exact-fit merged TAIL was truncated (or the pre-check
over-rejected it and flushed a truncated prefix). Both pre-checks now charge
the overhead only when no open entry is in progress, matching _push_locked's
open-aware bound. A regression case (the review's recipe: flush an open
fragment, then write one exact-fit newline-terminated line) is verified to
truncate when the +3 is restored.
The zh README's wire-contract section now describes the open flag like the en
side (the fd-3 Agent Note holds the split-billing arithmetic; a cross-doc link
was omitted to keep the bilingual link sequence aligned).
The review's warning: the one-byte overflow case ran through the CHILD ledger
(print path), so the host's first-fragment cap (logBudget - 1) never executed,
and the sub-2-byte guard test does not discriminate logBudget from
logBudget - 1 (a reverted cap still trips the guard). The frame is now forged
on fd 3, so a reverted cap of logBudget admits it and flushes it at settlement
— verified to turn the test red.
The review's dedupe suggestion: the split-billing arithmetic was stated in both
notes; the settlement note's Decision paragraph now links to the fd-3 protocol
note's wire-contract section (one home per fact), paired and re-recorded.
The review's suggestion: the settlement note's Decision section now states the
shipped split-billing fact (first fragment pays quotes+separator, continuations
and the closing frame pay content only; host caps logBudget-1 / logBudget+2;
child keys off _open_started), paired and re-recorded.
The review's suggestion: the open-merge mechanism (incremental split billing on
both sides, host caps logBudget-1/logBudget+2, the sub-2-byte walk guard, the
child's _open_started-keyed billing) lived only in code comments. The wire
contract section of the note now states it, paired and re-recorded.
The new jsonStringCostUpTo guard (returns undefined below a 2-byte cap) was
uncovered: forged open frames drive the host ledger down to one byte, and a new
open entry's first-fragment cap (logBudget - 1 = 0) trips the guard and
truncates to the marker, asserted as the merged entry plus the marker.
The review's arithmetic checks: the closing-frame walk used cap
logBudget - openCost, so a compliant merged entry (58-byte wire cost under a
64-byte budget) could see a negative cap and truncate; the first-fragment cap
used logBudget instead of the ledger's logBudget - 1, so an open frame costing
63 was admitted with a bill of 64, pushing the ledger negative and letting a
subsequent empty frame ride in one byte past the configured cap; and the child
billed a closing frame as a fresh entry (quotes+separator again) instead of the
merged tail, truncating an exact-fit 30+30 entry.
Fixes: first-fragment cap logBudget - 1 (matching admit), continuation and
closing-frame cap logBudget + 2 (billed without quotes), jsonStringCostUpTo
returns undefined below 2 bytes, and the child's split billing keys off
_open_started alone (a closing frame pays content only) with the cheaper bound
len(text) while a merge is open. Regression cases cover all three arithmetic
paths.
The review's critical: the open-merge branch re-joined and re-walked the whole
held text per frame, so k tiny open frames cost O(k * budget) (thousands of
1-byte frames against a near-64 MiB budget would re-traverse hundreds of GB and
block the host event loop). The host now holds a fragment ARRAY with an
incrementally billed cost — each fragment's jsonStringCostUpTo walks only its
own text — and the closing frame bills only its own content, so the merged
entry's wire cost is charged exactly once, split across the fragments. The
child bills symmetrically: the first open fragment pays quotes+separator, each
continuation pays only its content, matching the host ledger (the review's
warning: per-fragment full billing truncated a 16-char merged entry under
maxLogBytes: 64 that costs only 19 bytes as one entry).
Regression cases: 16 single-character flushes merge to one whole entry; a
closing frame that overflows the remaining budget truncates to the marker; a
closing frame after an open flood already truncated the ledger is a no-op; and
a forged open-frame flood stays bounded by the ledger. The closing-frame
post-truncation guard is an invariant-false branch (an open frame that would
trip the ledger resets openParts, so a non-empty hold implies no truncation)
and carries a v8 ignore with that reason.
The review's critical: the open-merge branch accumulated the held fragment
before any ledger check, so a forged open flood could grow host memory without
touching logBudget. The held fragment is now bounded by the exact-cost walk
(jsonStringCostUpTo against the remaining budget; the closing frame's admit()
still bills the merged entry once), and the open field is registered in the
README wire-contract section and the fd-3 protocol note (en + zh). A forged
open-flood case asserts truncation to the marker under a 64-byte budget.
The review's remaining warning: an explicit flush of an unterminated line
(print(..., end='', flush=True)) pushed a full log frame, so the following
print() landed in a second entry and logs.join('\n') rendered 'a\nb' for what
the program printed as one line — a model-visible output defect. The flush
frame now carries an flag (LogMessage gains the optional field on both
sides and in the mirror test), the host holds it and appends the next log frame
to the same entry, and finish() admits the residual if the run ends with it
still open. The settlement note registers the decimal-context fix from the
previous commit.
The review's critical: Decimal(repr(value)).normalize() read the process-global
decimal context, so a legitimate program setting getcontext().prec = 2 silently
rounded the completion value's digits and traps[Inexact] = True made the encode
raise, misclassifying a successful run as an exception. A fixed module-level
Context(prec=28) makes the spelling decision context-independent; a regression
case mutates both context knobs and asserts the float round-trips exactly.
The binding-reply README entry now states the fact (no seam-level cap;
maxValueBytes meters only the done frame; a wide reply is rebuilt and encoded
whole, bounded by process memory), matching the earlier reviewer wording.
The review showed the added dispose case was a placebo (dispose in the same
tick as run means SIGTERM hits the group before the program body runs; the
group-emptied arm is already deterministically covered by the same-group
survivor case, which this removes the v8 ignore for). The test is deleted; the
stale silently-discards comment in the boundary test now says rejects; the
README Known Limitations gains the late-log-frame-drop and host-side
binding-value-memory entries. Pairing re-recorded.
The review's premise that the group-emptied arm could not be pinned was
incorrect; the same-group reap case already exercises it. This adds the missing
seam-observable case: dispose() while a setsid orphan holds the pipes and the
run is unresolved — settle kills the child, the group empties (the orphan is in
its own session), and the poll finalizes promptly instead of waiting out the
60 s grace. The v8 ignore on that arm is removed.
The review's remaining coverage gap: the group-emptied arm of pollGroup depends
on the close-driven settle winning the race against the grace SIGKILL, a timing
interleaving no seam-observable test pins deterministically (the same-group
cases assert the settle and the reap, not this exact interleaving) — the arm
now carries a v8 ignore with that reason. The load-check comment and the
FRAME_ENVELOPE_BYTES JSDoc say rejects-as-worker-exit instead of drops.
The forged-second-boot-ack regression makes the re-entry guard covered, so its
v8 ignore is removed. Doc drift: the python README and run() JSDoc state the
resolve-with-value/resolve-with-error contract without inversion; the README
Known Limitations gains the setsid-escaped-orphan entry (the settlement note
referenced it); the settlement note drops the stale drops/discard phrasing and
the two 256 MiB references; the fd-3 protocol zh note no longer claims the
codec is undelivered; the code-runtime seam README (en + zh) says both
backends ship. Pairings re-recorded.
The review rejected the v8-ignore defense for the ack gate: a forged second
boot-ack is deterministically constructible (one os.write on fd 3) and the
run-write failure is deterministically constructible with the boot-write-failure
mock pattern. A program that forges an extra boot-ack asserts the run still
completes once (the gate does not re-send the run frame); a mocked child whose
fd-3 pipe accepts the boot frame but rejects the run write resolves a
worker-exit.
The resolvePythonBin directory branch now has a regression: a PATH whose first
entry is an executable DIRECTORY named python3 is skipped for a later real
interpreter (fail-before: without the isFile guard the directory would be
chosen and spawn would fail). The boot-ack gate's forged-second-ack re-entry
guard and its write-failure branch are covered by v8 ignore comments (the
honest child sends exactly one ack; the write failure needs the child to exit
between ack and write).
The review's two behavior items: the run frame was written back-to-back with
the boot frame (the seam contract puts run after boot-ack, which confirms the
namespaces were accepted); it now goes out from the boot-ack handler, so a
boot failure cannot race the run frame. resolvePythonBin now requires the
candidate to be a regular file — a directory passes X_OK and would otherwise
shadow a later real interpreter. Doc spots: the load-time overflow message
says worker-exit (not stranding to the wall clock), the run JSDoc spells out
the resolve-with-error contract, the PATH-stub test removes the stale v8
ignore, and the README's binding-value bullet names serialization cost.
The local machine's node_modules still links claude-agent-sdk 0.3.220, so a
local gen-third-party-notices run rewrites the file to that version; CI's
fresh install resolves the lockfile's 0.3.241 and gen expects it. The branch
adds no third-party dependencies (the schemastery workspace link is already
covered), so the notices file adopts master's 0.3.241 content.
The earlier merge had adopted master's protocol-only package.json (peer/dev
limited to invariants and cordis, no dependencies), but src/index.ts imports
@deepseek-ai/dsh-code-runtime, dsh-session, dsh-timeout, and schemastery at
runtime — a published lib/index.js could not resolve those bare specifiers.
The manifest now mirrors code-runtime-worker-thread (the five peers, the
schemastery dependency, and the matching dev set); the lockfile, module graph,
and third-party notices are regenerated, and the module-graph zh pair is
re-synced.
The branch's sdk-runtime manifest had carried a code-runtime-python workspace
peer that master's lockfile does not record, so a frozen install failed on the
mismatched specifier. The branch changes no sdk-runtime code, so it adopts
master's manifest verbatim.
The earlier merge had kept the branch's older package.json while taking
master's lockfile, so a frozen install failed on mismatched specifiers for the
code-runtime-python package (master added dsh-code-runtime, dsh-session, and
dsh-timeout peers). The branch changes no dependencies, so it adopts master's
manifest verbatim.
The merge conflict on pnpm-lock.yaml had kept the branch's older dependency
resolutions; the coverage gate's notices check then failed because CI's frozen
install resolved the master lockfile's versions while the committed notices
still named the branch's older ones. The branch adds no dependencies, so it
adopts master's lockfile and notices verbatim.
The audited library registry still listed dsh-code-runtime-python as a plain
protocol library, but the shipped package's src/index.ts has a plugin default
export; the entry is removed from PACKAGE_LIBRARIES and both READMEs declare
kind: package-reference. The zh README heading is 概述 per the standard.
The merge pulled master's README rewrite (front-matter, Summary, TOC, section
anchors, details-folding); its content described the pre-delivery protocol-only
package, contradicting the shipped backend. The README (en + zh) now follows
that structure with the delivered facts: PythonCodeRuntime, the fd-3 wire, the
load-validated caps, the 64 MiB frame parse cap (worker-exit settlement), and
the known limitations. Pairing re-recorded.
The review's three stale-comment items in index.ts: the orphan JSDoc above
FRAME_PARSE_CAP_BYTES (left over from the deleted receive ceiling), the
pre-join comment's change narration and its reference to a no-longer-existing
higher ceiling, and the first-frame comment's mention of a per-line cap check
that no longer exists. Test comments for the pythonBin and sealing-threshold
cases are weakened to their observable claims (both orders reject an over-cap
frame; the pythonBin case pins the contract, not a worker-exit distinction).
The fd-3 protocol note (en + zh) drops the 'future provider/runtime' staging
language (the runtime is delivered and its real-subprocess suite owns the
field-type gap), and the settlement note's Testing paragraph records the
frame-cap, multi-frame, sealing-threshold, and pythonBin resolution cases now
in the suite. Pairings re-recorded.
The review's doc drift items: the orphan receive-ceiling JSDoc, the frame-ceiling
references in index.ts/bootstrap.py/tests, and the README's 'dropped, stranding
to the wall clock' phrasing (the run now settles as a worker-exit) are all
updated to the 64 MiB FRAME_PARSE_CAP_BYTES semantics; the README notes the
>64 MiB binding-argument residual as a worker-exit trip of the same cap. A
regression case resolves a basename pythonBin against a PATH whose first entry
is relative ('.') and asserts the absolute entry is used.
The review's remaining code items:
- resolvePythonBin now skips RELATIVE PATH segments (a bare 'bin' or '.'): the
returned candidate must be absolute, because spawn() resolves a relative
pythonBin against the host CWD, outside the seam contract.
- A deterministic-ish regression pins the sealing-threshold corner: 64 MiB of
4 KiB (<= PIPE_BUF, atomic) newline-free writes plus 12289 more A's before
the first newline make the first frame exceed FRAME_PARSE_CAP_BYTES; the
newline-bearing chunk reaches the first-frame check (sealing is the ELSE
half of the newline branch), so the run reports worker-exit with the
protocol-frame-exceeded message.
The review's sealing corner: the fragment-count seal ran before the newline
branch and did not exclude a newline-bearing chunk, so the 1024th chunk (the
first to carry a newline) was concatenated into a sealed block, pendingChunks
was emptied, sawNewline stayed false, and the first-frame check was skipped for
a join that then contained the newline. Sealing now runs as the ELSE half of
the newline branch, so a newline-bearing chunk always reaches the join and its
first-frame check, and the invariant 'sealed blocks hold newline-free prefixes
only' is true — which is what makes the removed per-line check genuinely dead.
The pre-join counter (single unframed line) and the first-frame check
(newline-bearing chunk) reject any frame past FRAME_PARSE_CAP_BYTES before the
join, so every line reaching this loop is within the cap by construction — the
per-line check was dead code and its continue branch could never fire, failing
the per-file 100% coverage gate.
The pre-join check charged the whole unframed buffer, which legitimately holds
several frames each within FRAME_PARSE_CAP_BYTES: a first frame of exactly the
cap followed by a second frame crossed the counter and was misreported as a
worker-exit. The pre-join rejection now fires only while the held bytes are a
single unframed line (this chunk carries no newline); once a newline arrives,
a FIRST-FRAME check measures the bytes up to the first newline across the held
chunks (including sealed blocks) and rejects only that frame before the join —
keeping the peak at one copy of its wire bytes — while later frames in the
same buffer are handled by the restored per-line check. Regression cases: a
72 MiB newline-free buffer is rejected pre-join (fail-before: joining would
have doubled it); two within-cap frames whose combined buffer crosses the cap
both survive (fail-before: the unconditional counter check turns it red).
The unframed-buffer counter guard runs before every join and guarantees each
line is within FRAME_PARSE_CAP_BYTES, so the line-loop cap check was dead code
(its continue branch could never fire, failing the per-file 100% coverage gate
on index.ts). Removed with a comment explaining the invariant.
The pendingBytes guard now trips at FRAME_PARSE_CAP_BYTES (64 MiB) instead of
the 256 MiB wire ceiling, so the three tests that flood/pin frames against the
guard assert the 67108864 message and write a 64 MiB-based workload.
The review's remaining critical: the fd-3 data handler checked the unframed
counter against the 256 MiB wire ceiling, so a single 64-256 MiB frame was
fully Buffer.concat-joined (a second copy) and only then dropped in the line
loop — the peak-memory doubling the pre-join check exists to prevent, for a
frame the parser is guaranteed to discard. The counter is now checked against
FRAME_PARSE_CAP_BYTES before the join; the regression case asserts a worker-exit
with 'protocol frame exceeded' (fail-before: reverting to the ceiling turns it
green, proving the join path). FRAME_CEILING_BYTES is removed.
The rejection-cap fix now has its regression: a completion value whose class
name is 70 MiB of Ns asserts invalid-output, not worker-exit (fail-before:
uncapping the diagnostic turns it red).
The settlement note (en + zh) updates the remaining stale bound text, and the
fd-3 protocol note (en + zh) no longer claims protocol-only exports or a
missing Python codec. Pairings re-recorded.
The review's remaining items:
- _done_with_value's rejection branch now caps the _check_done_value diagnostic
through _cap_message (a reason embedding a hostile class name could otherwise
push the done frame past the host's 64 MiB parse cap, misreporting an
invalid-output run as a worker-exit).
- The settlement note (en + zh) updates three stale facts (load bound is now
parse-cap minus envelope at 67108800; the sink goes directly through the
bound primitives); the fd-3 protocol note (en + zh) no longer claims the
package ships protocol without the runtime; FRAME_ENVELOPE_BYTES' JSDoc and
_cap_message's docstring follow the new bound.
Pairings re-recorded.
The review's remaining warning: the _run binding comment claimed the log sink
went 'through the bound send', contradicting the sink's actual direct use of the
bound encode+write primitives. The comment now states that; the settlement note
(en + zh) registers FRAME_PARSE_CAP_BYTES and the 65 MiB-frame regression case.
Pairing re-recorded.
The review found the 64 MiB parse cap contradicted the load-time budget bound:
maxLogBytes/maxValueBytes could be configured up to ceiling - envelope (~256 MiB),
but the receive path silently dropped any frame past the 64 MiB parser cap, so an
honest child's budget-internal done frame under such a config would be discarded
and the run stranded to the wall clock. The load bound is now parse-cap -
envelope, so a configured budget always fits through the parser; the boundary
test moves to 64 MiB - 64. The >64 MiB model-constructed binding-argument drop
is registered as an accepted residual in the README (en + zh).
Addresses the review's remaining two items:
- FRAME_PARSE_CAP_BYTES (64 MiB) drops an fd-3 frame whose raw length exceeds
it BEFORE toString/JSON.parse: the 256 MiB wire ceiling bounds the bytes, not
the decoded structure, and a compact wide frame near that ceiling could decode
to far more host memory. A regression test writes a 65 MiB log frame plus a
normal one and asserts the oversized frame is dropped while the trailing frame
still lands in logs (fail-before: without the cap the oversized text is parsed
and admitted, truncating the ledger so the trailing frame is dropped). The
forged-oversized lower-bound test's frame is reduced to stay under the cap
while still exercising the truncation path.
- The log sink writes through the def-time bound encode+write primitives (not
send_sync, whose body resolves _encode_json_plain and self.write_encoded at
call time), so a rebind cannot break a log frame.
The review's remaining functional item: send_sync's body resolves
_encode_json_plain (module global) and self.write_encoded (class attribute) at
call time, so a program rebinding either before the first binding call could
turn a legitimate call into an exception. dispatch now writes the call frame
through def-time bound write_encoded+_encode_json_plain, and the log sink goes
through the bound send; the dispatch rebind test also rebinds those two names
(verified fail-before by reverting to send_sync). The annotation test title
matches its assertion direction, and the note (en + zh) registers the
error-class constructor, dispatch primitives, and dont_inherit mechanisms.
Pairing re-recorded.
The review required regression cases for the two cfb35bef6 fixes:
- Rebinding __main__.Exception/__main__.setattr must not break the minted
error class: a host rejection still surfaces as ToolCallError with the member
property readable.
- Rebinding __main__._lossless_json_violation/__main__.asyncio/
__main__.ProtocolChannel.send_sync must not break dispatch: a legitimate
binding call still round-trips.
bootstrap.py imports from __future__ import annotations; compile(wrapped) was
inheriting that PEP 563 flag, stringifying the program's type annotations and
changing the semantics of a legal program that reads f.__annotations__ at
runtime. compile(..., dont_inherit=True) stops the leak; a regression test
defines an annotated function and asserts the annotation is the live int class,
verified fail-before by removing dont_inherit (the test turns red).
The review's remaining items:
- _make_error_class captures Exception and setattr as def-time defaults, so a
rebind of __main__.Exception/__main__.setattr cannot break the rejection
constructor.
- dispatch binds _lossless_json_violation, asyncio.get_event_loop, and the
channel's send method into _run locals before the program runs, so a rebind
cannot turn a legitimate binding call into an exception or a wall-clock
timeout.
- The note (en + zh) corrects the stdin coverage phrasing: d3f9f57f5's direct
EOF-observing case is the in-tree pin, not an approximation.
- Collapse two stray double blank lines in the test file.
Pairing re-recorded.
The review's three remaining items:
- A regression test rebinds __main__.ProtocolChannel.read_frame_async and asserts
a binding reply still round-trips (the pump's reader is a bound method
captured by _run before the program runs).
- The settlement note (en + zh) records that send_done's frame-shape check uses
_run's bound _str/_isinstance.
- The staging-removal comment no longer claims teardown retries tracked state:
teardown deliberately does not sweep staging, so a removal failure is the one
case the gone-by-settlement contract degrades on.
Pairing re-recorded.
The review flagged that the implemented note's capture-family enumeration had
not followed c0ca236b5: read_frame/read_frame_async now also capture len (and
asyncio.get_event_loop on the async reader), _decode_json_plain captures
isinstance/str/list, and the reply pump's frame reader is injected as a bound
method captured by _run before the program runs. Note (en + zh) updated;
pairing re-recorded.
The stdin destroy (child.stdin?.destroy() right after spawn) previously had no
in-tree coverage. A program that reads fd 0 now sees EOF immediately; without
the destroy it blocks and the run would hang to maxWallMs as a timeout —
verified fail-before by disabling the destroy (the test turns red at the wall
ceiling) and restoring it (green). The _str rebind regression was attempted but
is not viable: the success path's done-frame serialization reaches str
transitively through _encode_json_plain, which the README Known Limitations
already records as the accepted success-to-exception residual, so any rebind
test trips that documented residual before send_done's bound _str.
The review's completeness check found the def-time capture pattern was not yet
applied to every name the reply/settlement paths resolve at call time:
- _decode_json_plain now also captures isinstance/str/list.
- read_frame/read_frame_async capture len; read_frame_async captures
asyncio.get_event_loop.
- send_done uses _run's bound _str/_isinstance for its frame-shape check.
- The reply pump's frame reader is a bound method captured by _run BEFORE the
program runs and passed into _pump_replies, so a rebind of the class
attribute cannot redirect it.
The decode-rebind regression test still pins the _decode_json_plain rebind;
rebinding builtins (len/isinstance/list/str) in a test is not viable because
the Python runtime itself resolves them implicitly.
The boot-write-failure fake child carries no stdin at runtime, so the optional
call is the documented guard; the static type (ChildProcessWithoutNullStreams)
says stdin is non-null, which trips the no-unnecessary-condition lint.
The boot-write-failure path's fake child carries no stdin handle, so the
unconditional destroy threw inside the spawn error handler and mislabeled the
worker-exit. Use the optional-call form; the no-stdin branch is exercised by
that same test.
Addresses the review's two remaining items:
- The host closes the child's stdin write handle immediately after spawn. The
program is an async body that reads nothing from fd 0; a live pipe would hold
a host-side handle open past the run, so a setsid-escaped descendant
inheriting fd 0 could keep the host process from exiting even after the
closeDeadline forced settlement. The child (and any descendant) reads EOF on
fd 0 and no host handle survives.
- read_frame/read_frame_async bind their decode primitives (_decode_json_plain,
os.read, _READ_CHUNK_BYTES, bytes) as def-time default arguments, and
_decode_json_plain itself captures json.loads, its two regexes, and len the
same way, so a __main__ rebind cannot kill the reply pump and strand every
pending Future to the wall clock. _decode_json_plain and its regexes moved
before the ProtocolChannel class so the defaults resolve at class-definition
time. A regression test rebinds _decode_json_plain and asserts a binding reply
still round-trips.
Note (en + zh) registers both mechanisms; pairings re-recorded.
The review's remaining non-blocking suggestion: dispatch's call_failure(str(exc))
resolved the builtin str at call time, so a program rebinding __main__.str could
run a hostile callable when the binding-rejection message is formatted. Bind
_str into _run locals and use it in dispatch.
dispatch's call_failure and its except clause resolved the module globals at
call time, so a program rebinding __main__._BindingRejection = ValueError let
the internal marker type leak into model code. Bind _RuntimeError_cls and
_BindingRejection_cls into _run locals before the program runs (names distinct
from the module globals so the assignment RHS resolves the global, not an
unbound local); dispatch now uses the locals. A regression test rebinds
_BindingRejection and asserts a host rejection still surfaces as RuntimeError.
The sys.__stdout__ flush test now reconfigures the streams back to block
buffering (write_through=False) so the settlement drain path is what the case
pins — verified fail-before: binding the stream objects instead of their flush
methods turns the test red.
The settlement drain iterated the bound stream OBJECTS, which are not
callable — every _flush() raised TypeError and was swallowed by the loop's
except, so the drain never ran and only the -u flag carried the behavior.
Bind sys.__stdout__.flush/sys.__stderr__.flush (bound methods, capturing the
stream at binding time, immune to a later sys.__stdout__ rebind; None-guarded).
Verified by removing -u temporarily: the sys.__stdout__ regression test still
passes, so the drain is a genuine backstop, not a documented-but-dead layer.
Addresses the review's two carried warnings and the comment suggestion:
- Once the ledger truncates, every arm that marks it (admit()'s two ceilings and
the child-marker frame arm) now clears both stray pipes' buffered output
wholesale, so the end-path flushStray sees empty buffers instead of
concat+decoding doomed data near a 256 MiB maxLogBytes; captureStray's newline
loop re-checks the flag before re-retaining the residual.
- The child runs with -u (unbuffered), so sys.__stdout__/sys.__stderr__ writes
are visible to stray capture immediately; the settlement flush still drains
the original std streams before the done frame as a guard. A regression test
writes through sys.__stdout__/sys.__stderr__ without an explicit flush and
asserts both bytes land in logs. C-ext stdio remains an accepted residual,
recorded in the README Known Limitations (en + zh).
- The ledger-comment arithmetic now states the exact boundary (serializes to
exactly maxLogBytes; without the reserved byte it would be maxLogBytes + 1)
in both host and child.
Note (en + zh) registers the stray-clear and -u/settlement-drain mechanisms and
the new test; pairings re-recorded; corpus passes 1029.
The review flagged the change-narrative wording 'degrades to the pre-existing
behavior' (prohibited by docs/AGENTS.md) in four spots — README en/zh, the
readProcessStart JSDoc, and the test comment — and the incomplete :77 residual
sentence ('can still' with no verb complement). Reword the four to a direct
statement of current behavior (killGroup signals the pgid without the identity
re-check on macOS), complete the residual sentence with the actual consequence,
and re-record both pairings. Corpus-wide verify-translation-pairing passes 1029.
The master merge brought a stale generated config-catalog that omitted the
dsh-code-runtime-python config section and mislisted the package. Regenerate
docs/config-catalog.md (verify-config-catalog passes), translate the python
config section into zh, keep the ts config-catalog code blocks verbatim
(untranslated, per the pairing rule), and drop the stray zh Library-packages
line. Corpus-wide verify-translation-pairing passes 1029.
The review found the 62 floor off by two (the marker's fixed prefix is 51
characters counting both square brackets, so marker(62) serializes to 63) and
the constructor error over-claiming a bound the marker-as-envelope design does
not deliver. Fixes:
- MIN_LOG_BYTES is 64 (marker-only serialization fits with one byte of room);
the JSDoc arithmetic counts the brackets; the rejection test pins 63; the
forged-frame test uses 11 NULs (69 escaped) at 64.
- The constructor error now states the marker-only guarantee, and the README
Known Limitations (en + zh) records the real bound: a truncated run with
admitted entries serializes its logs to maxLogBytes + marker + envelope.
- The SIGXCPU-mask tests burn with time.process_time() instead of wall-clock
perf_counter, so a contended CI runner cannot under-burn the budget.
- The settlement note (en + zh) records the 64 floor and the marker envelope
bound, including the zh pre-encode section that the earlier pass missed.
- The README constructor-rejection list names the maxLogBytes floor.
Pairings re-recorded; corpus-wide verify-translation-pairing passes 1004.
The review flagged four mechanism changes shipped without note registration:
- Log ledgers start one byte below the budget (outer-array envelope reservation)
and the constructor floors maxLogBytes at 62 (the smallest budget that can
serialize its own truncation marker plus the envelope).
- die_if_cpu_exhausted restores SIG_DFL before unblocking a program-masked
SIGXCPU, so a trap+mask program cannot run a re-masking handler at the unblock.
- ast.parse passes filename="<model>" so parse-time syntax diagnostics share the
compile/runtime source label.
Decision and Testing (en + zh) now record all four with their fail-before cases
(exact-limit, budget rejection, syntax label, SIGXCPU-mask, trap+mask); pairing
re-recorded and consistent.
Addresses the review's two code warnings and one suggestion:
- die_if_cpu_exhausted now restores SIG_DFL BEFORE unblocking SIGXCPU: a program
that installed a custom handler AND masked the signal would otherwise have
that pending handler run at the unblock (in model code, re-masking or raising)
and escape the re-raise; with SIG_DFL first the pending signal kills inside
the kernel with no bytecode window. A trap+mask combined regression test pins
it (the mask-only case was already covered).
- The constructor rejects budgets too small to honor: maxLogBytes must fit the
truncation marker plus the serialized outer-array envelope (floor 64), and
maxValueBytes must at least represent the smallest JSON completion (floor 4,
matching the worker backend). The exact-limit test moves to the 64 floor and
a rejection test pins the floors.
- The pthread_sigmask None-guard comment cites the real rationale (defensive
against stripped CPython builds; win32 is refused at construction), not the
unreachable Windows path.
The residual bullets listed the encoder's transitive deps as an exhaustive set
but disagreed with each other and omitted io. Mark the list as a non-exhaustive
example (e.g. _dump_scalar/_dump_string/json/io) in the README (en + zh) and the
settlement note (en + zh); pairings re-recorded and consistent.
Addresses the review's two remaining code warnings and the three suggestions:
- Log ledgers (host and child) start one byte below the budget, reserving the
serialized outer-array envelope (two brackets and n-1 commas over n entries'
separators); the exact-zero test moves to maxLogBytes 104 and a new exact-limit
case pins that maxLogBytes 5 admits ['a'] (5 bytes) while 4 truncates to the
marker alone.
- die_if_cpu_exhausted unblocks SIGXCPU (pthread_sigmask SIG_UNBLOCK, captured at
import, None-guarded for Windows) before re-delivering it, so a program that
masks SIGXCPU, burns past the soft limit, and returns is still classified as a
timeout; a regression test pins the masked path.
- ast.parse passes filename="<model>" so parse-time syntax diagnostics carry the
same source label as compile and runtime tracebacks; the syntax-error test
asserts the label.
- The NUL-escape test comments use the true six-byte JSON escape \u0000 instead
of the caret notation; the README Known Limitations (en + zh) records that
PID-reuse protection is inert on macOS; a combined-rebind regression test pins
BaseException plus the traceback reporter rebinding together.
Addresses the review's registration-text accuracy findings:
- _run binds _done_with_value into a local (done_with_value_bound) before the
program runs, closing the __main__._done_with_value = boom success-rewrite
vector; a regression test rebinds it and returns a legitimate value, asserting
the success survives.
- README (en + zh): the CPU-recheck bullet now states the recheck runs
unconditionally after the program returns (a pre-return overrun dies there as
a timeout) and the false-success window is only a trap-SIGXCPU program that
passes the recheck and overruns during the settlement flush/encode; the
encoder-deps residual rationale is replaced with the actual one (bash-equivalent
trust, verdict still delivered via the send_done fallback frame) and names the
now-bound entry; the t.join() deadlock bullet fixes the subject/object (the
main coroutine joins the worker, blocking the pump's main event loop).
- The portable-identifier-seam architecture note no longer claims the Python
backend does not exist.
- Settlement note (en + zh) registers the entry-name binding and the new test.
- All pairings re-recorded; corpus-wide verify-translation-pairing passes 1004.
Master reverted PR 2573, deleting the case-insensitive-path-round-trips
note that carried the dead link, so take the deletion and drop the note
repair from this branch; the jsonl.spec.ts change merges cleanly.
Addresses the review's two registration-text accuracy findings:
- The cross-thread t.join() deadlock is a process-isolation-backend property (the
pump runs on the child's main event loop), so it is split out of the wide-binding
REPLY bullet into its own Known Limitations entry with the correct attribution
(fix belongs in this backend, not packages/core/session); the zh half-width
space is removed.
- The settlement note's _done_with_value def-time default-arg sentence is
qualified: it guards a rebind of _check_done_value/_encode_json_plain, while a
transitive encoder dep (_dump_scalar/io) rebind can still downgrade, which is
registered as an accepted residual in the package README.
Pairing re-recorded; corpus-wide verify-translation-pairing passes 1004.
Document the two remaining keep-current residuals in the python package README
Known Limitations (en + zh), per the review's accepted-resolution path:
- A trap-SIGXCPU program can exceed the soft CPU limit during settlement encoding
and still report success (containment holds via hard +1s and wall clock; only
the classification is degraded, because the recheck cannot meter mid-encode).
- The encoder's direct deps (_dump_scalar/_dump_string/json) resolve at call
time, so a __main__ rebind after a legit return can downgrade success to
exception; the value path's top-level deps are def-time bound, the transitive
ones are an accepted residual.
Pairing re-recorded and consistent.
Addresses the bot's keep-current findings:
- The settlement note distinguishes the BaseException (lost done frame) and
RuntimeError (pump killed -> replies stranded to the wall clock) consequences;
registers the _done_with_value def-time default-arg capture and the new
RuntimeError-rebind closed-loop test; zh:95 half-width space fixed.
- The python package README Known Limitations records the cross-thread binding +
sync t.join() deadlock (en + zh).
- The code-runtime Service Definition README no longer claims only the
worker-thread backend ships: the Python (process) backend is acknowledged,
with 'container' as future work (en + zh).
- All pairings re-recorded; corpus-wide verify-translation-pairing passes 1002.
A body-local X = X binding in _pump_replies is too late: _run reaches the
model's top-level statements (which run first, since there is no suspension
point between create_task and await __dsh_main__) before the pump's first step,
so a __main__.RuntimeError rebind there would be captured by the body local and
a closed-loop failure would escape the except, killing the pump. Bind
_RuntimeError, _BindingRejection, str, and bool as DEF-TIME default arguments of
_pump_replies (evaluated at import, before any model code runs). Add a regression
test that rebinds __main__.RuntimeError as the first program statement and drives
the closed-loop worker pattern, asserting the pump survives and delivers the
later binding. Update the settlement note (en + zh) to describe the default-arg
capture; pairing re-recorded and consistent.
- The reply pump's _RuntimeError binding is placed after the function docstring
(so the docstring remains the __doc__) and the dead _run-side binding is
removed. _done_with_value binds _check_done_value/_encode_json_plain as
default arguments so a __main__ rebind after model execution cannot rewrite a
success into an exception.
The _str/_bool/_BindingRejection pump bindings were attempted but break the
closed-loop pump test (the self-referential _BindingRejection local interferes
with the closure), so they are left unbound; rebinding those names (builtins and
one internal class) is outside the practical threat model.
The previous commit bound _RuntimeError in _run, but _pump_replies is a separate
module-level function, so its except _RuntimeError referenced an out-of-scope
local and raised NameError instead of catching the closed-loop failure — killing
the pump and timing out the run. Bind _RuntimeError at the top of _pump_replies
too. The closed-loop pump test now passes.
The reply pump's except RuntimeError resolved the module global at runtime, so a
__main__.RuntimeError rebind could make a closed-loop scheduling failure escape
the catch, killing the pump and stranding every later reply. Bind RuntimeError
into a _run local alongside BaseException and catch the local. The settlement
note Decision now records that the exception classes the settlement-path except
clauses catch are bound into locals / a closure cell before model code runs
(en + zh); pairing re-recorded and consistent.
The rebindable-BaseException vector the bot flagged existed in every except
clause of the settlement path, not just the _run outer catch: safe_model_traceback
(three guards) and the post-done flush swallow resolved the module-global
BaseException at runtime, so a __main__.BaseException rebind plus a throwing
__str__ could let a render-time exception escape and lose the done frame. Bind
BaseException into a _run local (at the top) and a closure cell in
_make_failure_reporter, and change every such except clause to catch the local
— immune to a one-line rebind.
The zh Consequences section counted ten but enumerated only nine; add the
log-fragment seal as the 10th no-fail-before item. Also unify the term to
'封存' (matching the Decision/Testing sections) instead of '封口'. Pairing
re-recorded and consistent.
The _run outer try/except used the module-global BaseException, which the
program (running as __main__) can rebind: __main__.BaseException = RuntimeError
made the except resolve to RuntimeError, so a subsequent ValueError escaped _run
with no done frame and misreported the run as worker-exit. Bind BaseException
into a _run local before the program runs so the catch is immune; a regression
test rebinds BaseException and raises, asserting an exception, not a worker-exit.
Also correct the NUL-escape comment text: the JSON escape-result side is \^@ (6
bytes, the valid JSON NUL escape), not \x00, so the 6x-budget arithmetic in the
comments is self-consistent. Register the BaseException-rebind case in the
settlement note Testing (en + zh) and re-record the pairing.
Addresses the bot's keep-current review findings:
- The module-level fallback comment now states the mechanism truthfully: the
module globals are RAW primitives bound into _run LOCALS before the program
runs (the immunity lives in the frame-local binding, not the module global);
and the fallback literal <unrenderable> is distinguished from the failure
reporter's _UNRENDERABLE_DIAGNOSTIC text.
- The settlement note's fallback mechanism wording, the transitive-name rebind
case (now listing the three fallback primitives), and the no-fail-before count
are aligned en/zh; the zh Problem paste damage is fixed and the Consequences
count is ten with the 10th item.
- Pairing re-recorded and consistent.
Addresses the keep-current review findings:
- The settlement note's Problem/Consequences count is nine -> ten, adding the
log-fragment seal to the no-fail-before enumeration (its 25 M-scale OOM is not
deterministically constructible in CI); the new Decision section title now
names all four mechanisms and the double blank line is removed.
- README Known Limitations (en + zh) documents the 1-second dual-limit
ulimit -t 1 CPU overrun being reported as worker-exit (the hard >= 2 guard
cannot lower a 1-second soft to 0); pairings re-recorded and consistent.
The done-frame fallback read _os_write/_memoryview/_FALLBACK_DONE_FRAME as module
globals at call time, so a single-line rebind of any of them reopened the
rebind hole the fallback exists to close. Bind them into _run locals before the
program runs, and use a bare except (which catches everything without naming
BaseException, so a rebind of that name cannot defeat the handler). The
transitive-name rebind test now also rebinds _os_write/_memoryview/
_FALLBACK_DONE_FRAME to pin the fallback's immunity.
Keep the settlement note current with the latest code-review fixes:
- New Decision section for the _LogStream fragment seal, the _clamped
RLIMIT_CPU soft-lowering (and its hard==1 blind spot), the send_done
fallback frame, and the reply-queue slot release.
- Testing registers the fragment-cap drip (no-fail-before), the dual-limit CPU
overrun, and the transitive-name rebind cases.
- zh mirrored; settlement-fixes.i18n.yaml re-recorded and consistent.
Addresses the bot's follow-up review findings on the settlement-path fixes:
- The _LogStream seal joined the WHOLE accumulated buffer past the fragment cap,
re-copying the growing block O(B^2/cap) times for a large drip. It now seals
only the current fragments into a _pending_blocks entry (character count
unchanged), so a 25 M single-character drip stays O(B); the newline/flush/
_push_bounded_prefix consumers join blocks + fragments once.
- The _clamped soft==hard lowering is scoped to RLIMIT_CPU: for RLIMIT_AS a
one-byte soft differential would only misalign the child's applied limit with
the host-side budget gate, with no signal to preserve. The hard == 1 blind
spot is documented.
- send_done's fallback captures memoryview at import (_memoryview) alongside
os.write, so a one-line rebind of the name cannot change the fallback write;
the comment now states the module-level-captured mechanism.
The dual-limit CPU test used ulimit -t 1 (hard == 1), which the _clamped
soft-lowering guard (hard >= 2) intentionally does not lower, and trapped
SIGXCPU (which defeats the fix). Use ulimit -t 2 (hard == 2, so the soft is
lowered to 1) and leave SIGXCPU unhandled; the run then classifies as a timeout.
The message is the CPU-time-exhausted diagnostic, not the literal 'SIGXCPU'.
Addresses the bot's v16 review on the settlement-path code:
- critical: _LogStream._pending now seals the fragment list past a chunk cap
(like the host captureStray seal), so a newline-free single-character drip no
longer accumulates one list slot per write and OOMs on its own accounting.
- _clamped lowers a soft==hard result by one unit (when hard >= 2) so a
dual-limit ulimit -t leaves SIGXCPU a window to fire and a definite CPU
overrun is reported as a timeout, not a worker-exit.
- send_done wraps its encode+write in a try and, on any throw from a rebound
transitive name (_dump_scalar/os), writes a fixed pre-encoded done frame via
the import-time captured os.write, so a settled exception verdict is never
downgraded to worker-exit.
- drainReplies clears the consumed replyQueue slot so a wide written payload is
released immediately, bounding host memory to the current backlog under
sustained fd-3 backpressure.
Tests added for each (fragment cap drip, dual-limit CPU overrun, transitive-name
rebind done frame).
The Testing sentence's subject attached the three rebinds to 'the fix' rather than
to the fixture that performs them; reword to 'pinned by a case that rebinds' and
mirror zh ('由一个…用例钉住'), re-recording the pairing.
The rebinds-every-name fixture previously only rebound ProtocolChannel.send_sync,
which a bound method object ignores and the shipped send_done no longer calls —
so it did not actually guard the call-time-lookup shape. Rebind write_encoded
and _encode_json_plain too (the names send_done would resolve late if it looked
them up at call time) and state that in the settlement note's Testing section
(en + zh), re-recording the pairing.
Keep the agent note current with the recently landed code-review fixes:
- six -> nine no-fail-before cases, adding the done-value TOCTOU pre-encoding,
the stray-UTF-8 budget-flush retention, and the late-rejection settled guard,
each with its reason for not carrying a fail-before test.
- New Decision sections for the pre-encode + send_done binding and the stray
flush retention; Testing lists the binding-all-names case as a tested fix.
- zh mirrored; settlement-fixes.i18n.yaml re-recorded and consistent.
Each shift() re-slices the remaining array, so draining a large gather of
wide bindings awaiting fd 3's drain was O(n^2). Reading by a head index into
the array keeps the drain linear; the finally still discards everything.
Addresses the follow-up review findings on the settlement-path fixes:
- send_done now routes both the pre-encoded VALUE frame and the dict ERROR
frame through a bound _encode_json_plain + bound write_encoded, never through
channel.send_sync (whose body re-resolves self.write_encoded and the module
_encode_json_plain at call time) — a program rebinding ProtocolChannel.
write_encoded or __main__._encode_json_plain no longer skips the done frame.
- flushStray retention re-accrues the withheld multibyte tail from a FRESH
utf8 state (previously metering the carried lead against the post-flush
expected>0 state charged it as an illegal continuation), and skips admitting
when the whole residual drained into the retained tail so no bogus empty
entry is pushed.
Corrections to the settlement-path review fixes:
- send_done was invoking channel.send_sync / channel.write_encoded via a late
method look-up, which a program running as __main__ could rebind through
__main__.ProtocolChannel.send_sync before the failure path ran — a rebound
send that raises then skipped the done frame and downgraded a settled
exception to worker-exit. Bind both channel methods into locals before the
program runs, mirroring the pre-existing binding of flush_out/flush_err/
safe_model_traceback.
- Restructure flushStray so the mid-sequence budget-flush retention arm is a
self-contained v8-ignored branch and the covered default path decodes the
full residual (not schedulable-through-the-seam boundary).
Pace-free completion framing, stray UTF-8 flush, and late-rejection guards:
- Pre-encode the completion value at its validation point so send_done never
re-walks a live value a mutating daemon thread could change (TOCTOU); a
mutation-induced encode throw is then classified as 'exception', not a
host-side worker-exit.
- Budget-triggered stray flush retains an incomplete multibyte UTF-8 tail
(<=3 bytes) as residual instead of decoding a legal, split character to
U+FFFD in an admitted entry; the end/closeDeadline paths still full-decode.
- Check 'settled' before formatting a late binding rejection's message, so a
hostile message getter cannot stall or exhaust a run that already settled.
- Document _check_done_value's first-to-trip ruling in its docstring.
- Rewrite ProtocolChannel.send_sync around a shared write_encoded that the
done frame's pre-encoded string path uses.
The drain loop's `if (settled) break` needs the run to settle in the window
between two queued frames. A file probe on the concurrent-replies case shows the
queue does reach depth 11, but the wall clock never lands inside that window, so
the branch is not schedulable from a test; a case written to force it passed
without ever executing the line, so it is removed rather than left as coverage it
does not provide. The branch carries a v8 ignore naming what is unreachable.
`sendReply` ignored `proto.write`'s `false` return, so a program resolving
several large values in one `asyncio.gather` round encoded every reply in the
same turn and queued all of them in fd 3's writable buffer. Binding resolution
carries no seam-level byte cap to bound that, and the failure kills the host
process rather than failing the run: measured on a 64 KiB-highWaterMark pipe,
eight 4 MiB replies buffered 32.0 MiB at once against 0.0 MiB once paced.
Replies now go through a queue that encodes and writes one frame at a time,
awaiting `drain` when the pipe is full. The encode happens inside the loop, so a
queued reply the run no longer needs is dropped by the `settled` check without
ever being serialized.
This was previously deferred on the grounds that serializing would narrow the
seam's concurrency contract. That reasoning was wrong: the child matches each
reply to its `call` by id from a pump that reads fd 3 continuously, so arrival
order was never observable, and the bindings still run concurrently. Only the
host's peak memory and the flush timing change. The README entry recording the
deferral is removed and the Agent Note records the mechanism instead.
`sendReply` already refuses to write after the run settled, but only after
`snapshotJsonValue` walked and copied the resolution. Binding resolution carries
no seam-level byte cap, so a binding resolving a wide value after `maxWallMs`,
an abort, or dispose settled the run spent host heap building a frame that was
then discarded. The check moves ahead of the snapshot.
Also in this change:
- `readProcessStart` moved after `messageOf`. Inserting it between `messageOf`'s
JSDoc and its body left that function undocumented and the orphaned block
reading as a second doc for the reader; `verify-export-jsdoc` does not catch it
because `messageOf` is not exported.
- The README pair adds the disposed-runtime rejection to `run()`'s public
contract, which `src/index.ts` has enforced all along.
- Known Limitations records three deferred constraints that until now existed
only in review discussion: the combined log-and-value peak the load gate does
not model, the host-side per-member expansion of a wide binding reply (owned by
`packages/core/session`, and shared with the worker-thread backend), and the
absence of fd-3 backpressure for concurrent replies.
- The Agent Note's same-group section records the teardown identity guard and its
two rulings, including why an ABSENT start-time reading proceeds rather than
withholding the signal, and that reading it as a mismatch is what turned the
three same-group heartbeat cases red on Linux.
The directive carried its whole justification inline at 203 characters, past the
140 the @stylistic/max-len rule allows (imports and template-literal messages
are exempt; a line comment is not). The reasoning moves to the lines above and
the directive keeps a short pointer, since a v8 ignore must stay on one line.
The PID-reuse guard has two arms no single OS can execute: the non-Linux early
return in readProcessStart (the Linux coverage lane always takes the read path)
and the refusal arm, which needs a real pid recycled into a new group leader
between spawn and teardown -- no test can schedule that. The coverage lane
reported 99.53% statements / 99.14% branches on src/index.ts for exactly these
two.
Both carry a v8 ignore naming what cannot be reached and why, the convention
this file and subprocess-local already use for platform defenses. The reader
itself stays covered by the process-identity test rather than being exempted
wholesale.
The PID-reuse guard refused to signal whenever the current reading differed
from the one taken at spawn, including when it was ABSENT. On Linux a reaped
leader has no /proc/<pid>/stat, so every teardown after the leader exited
skipped SIGTERM/SIGKILL while the group it led still held survivors -- the
exact case the process-group teardown exists to reap. Three same-group survivor
tests went red on the coverage lane; they pass on Darwin because the reader
always returns undefined there, leaving the guard inert.
Only a present-and-different reading now blocks the signal. Verified on the
self-hosted Linux box: a reaped leader with live survivors allows the signal, a
pid whose start time differs still blocks it, and a live matching process is
signalled.
The README pair described `run()` as rejecting "a malformed binding namespace or
non-positive config", which understated and misplaced the configuration
failures: a non-Unix platform, a non-integer budget, a timer value setTimeout
would clamp, a budget larger than one fd-3 frame, and an incompatible
addressSpaceMb/output-budget pair all throw from the CONSTRUCTOR, so they fail
when the plugin loads rather than on a later run. Both sides now separate the
load-time platform/configuration errors from the run-result contract.
The Chinese README's Model Experience and KV Cache effect sections were still
untranslated English; the pairing record only tracks hashes, so it could not
show that. Both are now translated.
Adding a published Python backend and reordering `flush_line` left several
owning documents stating things that are no longer true.
`src/invariant.ts` justified its empty installer with "ships only the fd-3
wire-protocol codec", which the subprocess execution path contradicts. The
reason now states the actual one: every relation this backend maintains lives
in the CPython child or on the fd-3 wire, so no same-process event sequence is
observable from a listener -- the same shape the sibling worker-thread backend
uses.
The seam's `PORTABLE_RESERVED_WORDS` and `language` JSDoc, the code-runtime
README pair, and docs/subsystems/code-runtime both said only TypeScript has a
published backend. Corrected in all four, with the generated cordis catalog
regenerated for the `language` change.
The note attributed the 12x multiple to the settlement flush holding three
copies. That stopped being true when `flush_line` was reordered to drop the
pending chunks before its push: the binding worst case is the newline path's
single near-budget write. Corrected in the note (both sides) and in the test
comment that repeated it.
The note's Testing section now registers the cases this stack added, and the
Chinese side receives the O(depth) entry it never got plus the new ones -- it
had drifted from the English.
`INTERPRETER_BASELINE_BYTES` argued 64 MiB from a RESIDENT set while RLIMIT_AS
bounds address space. It now cites the bootstrap's own measurement (30.23 MiB
of mappings for `python3 -I`), making 64 MiB roughly twice the measured
baseline.
Also: a hardcoded `(:232-235)` comment reference becomes a reference by name,
a "which now walks in O(depth) too" change narrative becomes a current-state
statement, and a stray double blank line is removed.
Four independent corrections in the run lifecycle.
`killGroup` signalled `-child.pid` with a raw `process.kill`. Node keeps the
numeric `child.pid` after the leader is reaped and only clears its internal
handle, so `child.kill()` refuses while the raw call does not; `close` can
trail `exit` by seconds when a pipe-holding descendant keeps the streams open.
A recycled pgid could therefore receive this run's SIGTERM and armed SIGKILL.
`groupEmpty()` does not cover it: it reports whether the group has members, not
whether they are ours, and it first runs after the signal. The leader's start
time is now read at spawn and re-checked before each signal, matching the
position packages/subprocess/subprocess-local already states
("ProcessIdentity ... preventing teardown escalation after PID reuse"). Kept
local rather than depending on that package, which would add an architectural
edge. Linux reads /proc; Darwin has no /proc, so the reader reports undefined
and the guard degrades to the previous behavior instead of forking `ps` on a
teardown path.
`_push_bounded_prefix` built `(*self._pending, extra)`, copying every pending
reference into a same-size tuple before the bounded loop. For a
single-character drip that is a second pointer array as large as the list:
measured +80 MiB of tuple over a 40 MiB list for 5.2M chunks, the allocation
the bounded prefix exists to avoid. It now iterates the list in place and
handles `extra` in the loop's `else`; 4000 randomized inputs produce byte-identical
prefixes.
The settlement `flush_out()`/`flush_err()` ran outside any guard while `done`
was already decided, so a flush raising under memory pressure skipped
`send_done` and downgraded a child-classified `exception` into a host-side
`worker-exit`. Both are now wrapped, swallowing only the log tail.
The boot re-check's `if effective_soft != RLIM_INFINITY` was dead: `_clamped`
is asked for a finite `addr_bytes` on both sides and each branch returns that
value or a `min` with an inherited bound, so RLIM_INFINITY is unreachable. The
guard could only ever have skipped the re-check it claimed to protect.
Three separate paths in the CPython child allocated state proportional to a
value's width or a string's length, so a legitimate input the byte budgets
admit could die as the program's own MemoryError.
`_lossless_json_violation` enqueued one traversal tuple per member while
running, in `dispatch`, over MODEL-CONSTRUCTED binding arguments that no
child-side byte budget bounds first. It now uses the same (kind, container,
iterator) cursor the other two walks already had, checking dict keys as the
cursor pulls each entry. Measured over `[0] * 6_000_000` (~17 MB of JSON):
459.1 MiB of traversal tuples before, 0.0 MiB after.
`_decode_json_plain` matched JSON strings with a `(?:[^"\\]|\\.)*` repetition,
which makes CPython's engine retain backtracking state proportional to the
string's width: 146 MiB for a 1 MiB string, 557.8 MiB for 4 MiB. A legitimate
multi-megabyte binding reply raised MemoryError inside `_pump_replies`, and
because that pump is the only settler of the call's future, the run stranded
until the wall clock reported `timeout`. Strings now scan chunk-to-chunk over a
character class, which the engine matches without backtracking state; the same
4 MiB decode peaks at the 4.0 MiB result.
`_check_done_value` charged strings and dict keys what
`_dump_string(...).encode()` returned, building the escaped copy plus its
encode to MEASURE it -- ~6x the original each for control-heavy text, so
metering a value the budget then rejects could itself breach RLIMIT_AS and
report `exception` where the seam promises `output-limit`. The new
`_json_str_cost` counts instead, reusing `_json_string_cost`'s C-level passes
and reproducing `_dump_string`'s exact surrogate rules (fold spelled-out pairs,
charge six ASCII bytes per lone surrogate). Identical values, 228.9 MiB -> 19.1
MiB of peak on a 20M-NUL string.
Each fix ships a regression test. The two RLIMIT_AS repros are Linux-only:
Darwin does not apply the limit, so the peaks above are measured directly and
recorded in the test comments.
The O(depth) wide-value regression test ran under `maxWallMs: 20_000`, but the
cursor pulls 6M elements one at a time through Python-level frames: ~11s on an
idle machine, and more under the coverage lane's V8 instrumentation with several
workers sharing a runner. CI reported `timeout` instead of the round-trip.
Raise the run's ceiling to 60s inside a 90s vitest timeout, so the runtime's own
wall clock still fires first on a genuine hang. The assertion is unchanged and
still discriminates: restoring the O(width) `stack.extend` enqueue fails the test
with a child-side MemoryError in ~2.6s.
`_check_done_value` and `_encode_json_plain` pushed one stack entry per child
(plus a separator marker, and `dict.items()` materialized as a list), so the
bookkeeping scaled with the value's WIDTH rather than its depth. A value the
byte meter admits could then die on the walk's own frames: a flat
`[0] * 2_000_000` serializes to 4.0 MB, but measured peaks were 145.2 MB in the
meter and 114.7 MB in the encoder — 28.7x the serialized size, far past the 12x
the load-time address-space gate reserves.
Each container now pushes ONE cursor frame that pulls its children one at a
time and writes into a shared `io.StringIO`, so the output string is the only
width-proportional allocation and the caller already metered its size. Measured
on the same value: 0.0 MB in the meter and 9.0 MB in the encoder (2.3x), with
identical verdicts.
The load gate bounds maxLogBytes and maxValueBytes independently against the
address space, but the child framed the completion value (materializing its
escaped form to meter it, then encoding the frame) while a newline-free log tail
still sat unflushed in _pending. Those two peaks added, so two budgets each
admitted alone could together breach RLIMIT_AS and die as worker-exit instead of
settling. The success path now flushes both log streams before _done_with_value
runs; the trailing flush stays for the exception path and is an idempotent no-op
after a successful settle. A combined-peak regression test (32 MiB each against
512 MiB) asserts the over-budget value reports output-limit rather than OOMing.
Also corrects the worst-case-multiple JSDoc and Agent Note: after 1088d6f03d
made flush_line drop pending before its push, the settlement-flush path holds
two copies, not three, so the newline path is the sole 12x worst case. The
reorder is recorded as a called-out untested fix (the 12x gate already admits
only configs safe under both flush orders).
Bring in master through cf6750b10 (including the gate-runner fail-fast
change) and drop the stale fixture reference in the
case-insensitive-path-round-trips note: PR 2573 linked a fixture that PR
3128 had already removed, so every pull request's markdown-links gate
failed on master's own note. The claim stands without naming the deleted
file; verify-md-links now resolves all 2199 files locally.
The load-time output-budget/addressSpaceMb gate used a worst-case multiple of 8,
assuming two simultaneous ~4x astral copies (the built string and its encode).
Three are live at the peak: on the newline path a single write holds the caller's
text argument, the line slice handed to push, and push's encode copy; the
settlement flush_line path held the pending chunks, their join, and that encode
copy. A budget admitted at 8x (e.g. maxLogBytes 48 MiB against addressSpaceMb 512)
could still OOM the child. The multiple is now 12, the strict `>` is `>=` so a
budget whose peak exactly equals the room left after the interpreter baseline is
rejected (that peak plus the baseline is the whole address space), and flush_line
drops the pending chunks before its push to match the newline path's
join-clear-push order. The child re-check mirror and both note sides move in step;
config-catalog is regenerated from the updated field JSDoc.
The boot re-check raises inside bootstrap's setrlimit-phase handler, which
classifies every resource-limit-application failure as kind 'exception'. The
test asserted 'worker-exit'; align it to the actual class and keep the message
assertion so the case still discriminates a config rejection from a generic
setrlimit error. The Agent Note's two references to the reported kind are
corrected on both language sides and the pair re-recorded.
The output-budget/address-space gate's 8x multiple had no room for the
interpreter's own footprint, so a budget sized right at addressSpaceMb/8 was
admitted while its worst-case peak plus the interpreter overran RLIMIT_AS
(e.g. 15 MiB maxLogBytes against 128 MiB). Reserve a fixed
INTERPRETER_BASELINE_BYTES (64 MiB) before the multiple claims the rest, so each
budget times 8 must fit the room LEFT after the baseline.
The host gate validates against the CONFIGURED addressSpaceMb, but a launch
environment can inherit a stricter RLIMIT_AS (a ulimit -v wrapper below
addressSpaceMb) that _clamped lowers the effective limit to, leaving the budgets
sized for a ceiling the child never gets. bootstrap.py now re-checks both budgets
against the effective clamped soft limit after applying it, mirroring the host
gate's multiple and baseline, and raises at boot rather than letting a
near-budget output OOM mid-run.
Add regression tests for both (the load gate against a 256 MiB address space
covering both budgets, and a ulimit -v wrapper for the inherited-limit re-check);
register the tail-copy test in the note Testing section; sync the zh pair. Merges
origin/feat/code-runtime-python-protocol to resolve the DIRTY base.
The tail-copy regression built `"first\n" + "A" * 200 MiB`, whose construction
alone peaks near 400 MiB (the string plus the concat temporary) and OOMs under
the 384 MiB addressSpaceMb before the log path under test runs — a MemoryError in
the model, not the defect. Build the tail in a variable and concatenate only the
newline (peak ~2x150 MiB = 300 MiB, under the address space), so the model's own
allocation fits; the pre-fix code then buffered the whole 150 MiB tail again,
pushing past 384 MiB, while the sliced prefix does not.
The load-time addressSpaceMb gate used a 1/8 fraction derived for ASCII, but the
child ledgers trigger on character count against a serialized-byte budget: an
astral character is one character yet ~4 bytes stored and ~4 encoded, live at
once, so the true worst-case peak is ~8x the budget, not ~2x. Replace the
fraction with an explicit OUTPUT_BUDGET_WORST_CASE_ADDRESS_SPACE_MULTIPLE (8)
and a strict `>`, and gate maxValueBytes the same way as maxLogBytes — the value
path builds and encodes a near-budget completion under the same RLIMIT_AS, so
the incompatible pair was previously admitted there too.
Slice the newline branch's unterminated tail to a budget-sized prefix: it
buffered the whole text[pos:] before the flush trigger could bound it, so an
early newline plus a huge tail made a second full copy of the model's string —
an RLIMIT_AS death the config gate cannot cover since the tail can far exceed
maxLogBytes.
Disclose the cross-field constraint in the maxLogBytes/maxValueBytes/addressSpaceMb
JSDoc (regenerating config-catalog); refresh the note's stale
Buffer.byteLength(JSON.stringify) reference; reconcile the arrival-order rebuttal
with the seam's "in order" logs JSDoc (within-stream, cross-stream best-effort).
Extend the load-rejection test to both budgets and add a tail-copy regression;
sync the zh pair.
The child log ledger encodes an admitted entry to UTF-8 once to charge its
serialized cost, so a maxLogBytes approaching addressSpaceMb lets a legitimate
near-budget log entry breach RLIMIT_AS and die as worker-exit instead of
truncating. Two runtime fixes were tried and both traded one resource bound for
another: an exact serialized-cost check is either a full encode (the allocation
being avoided) or a per-character Python loop that burns the CPU budget (a 10 MB
write hits SIGXCPU under cpuSeconds:1). The breach is a property of the
maxLogBytes/addressSpaceMb pair, not any write, so reject the incompatible pair
at load — maxLogBytes must stay within one eighth of the addressSpaceMb byte
count — and revert _LogStream to its original character-count buffering, which
is memory-safe once the budget fits the address space. The check runs on every
platform since the incompatibility is a config-value property, not a runtime one.
Replace the child-flood regression tests (which asserted the reverted runtime
behavior) with a load-rejection test. The host-side accrueStrayCost UTF-8
per-lead validation and its tests are unaffected. Update the note and zh pair.
accrueStrayCost accepted any 0x80-0xBF continuation, so a CESU-8 surrogate
(ED A0 80) or overlong (E0 80 80) — structurally well-formed but illegal, and
as cheap to flood as 0xFF — was charged its structural width 3 while
toString('utf8') renders each byte as its own U+FFFD (cost 9). Validate each
lead's first-continuation range (WHATWG E0/ED/F0/F4 bounds) and charge 3 per
byte of any sequence outside it, folding a broken prefix to one U+FFFD.
The child _LogStream newline path had the same char-vs-serialized gap the
newline-free trigger had: its per-line fit checks (first reconstructed line and
each subsequent line) compared character count against the serialized-byte
budget, so a control-char line passed and _logs.push encoded it whole, breaching
RLIMIT_AS. Route every check through _fragment_cost_upto, which sums per-char
costs from _json_char_cost over a start/end sub-range without slicing or
encoding and stops at the budget.
Decline arrival-order stray flushing: the two pipes' data events interleave
nondeterministically and logs carries no cross-pipe ordering guarantee, so a
fixed drain order is as valid as any and an arrival-tick branch could not be
covered without a flaky test.
Add CESU-8/overlong, newline-path-flood, and all-lead-class reassembly
regression tests; fix the note's now-inaccurate CESU/illegal-byte claims and a
fixture byte-count comment; sync the zh pair.
The prior child-flush fix measured each fragment with chunk.encode('utf-8'),
which copies the whole write — under a tight addressSpaceMb a single 340 MiB
write died on that encode (the exact allocation _push_bounded_prefix exists to
avoid), and re-scanning the whole pending list per write was quadratic under a
daemon-thread flood (the concurrent-write test timed out at 28s). Compute each
fragment's serialized cost with _fragment_cost_upto, which walks the str via a
new _json_char_cost (code point to escaped width, no encode) and stops once the
running total passes the budget, and accumulate it into _pending_cost once per
write. The early-flush trigger reads that accumulator: still charges control
chars their full serialized width (a NUL is 6 bytes), but never encodes a whole
write and never re-scans the buffer, so the 340 MiB single-write and
daemon-thread tests pass alongside the NUL-flood one.
Rework the child NUL-flood regression to write in 1 MiB chunks under a 512 MiB
address space so its own argument construction is not the allocation under test.
The child-log-flood regression built one 30M-char argument string, which under
the 64 MB addressSpaceMb died on RLIMIT_AS during construction (exit 120) before
the flush trigger under test could run, so it failed on Linux CI. Write the
flood in 1 MiB chunks under a 512 MiB address space instead: the argument str is
never itself the allocation under test, the fixed serialized-cost trigger keeps
the pending tail bounded to a few MiB, and the run completes at the marker; the
pre-fix char-count trigger accumulates the whole ~200 MiB and its ~1.2 GiB
settlement encode breaches RLIMIT_AS. Mirrors the addressSpaceMb budget the
existing oversized-completion tests use.
The host stray-capture cost function charged illegal UTF-8 bytes (0x80-0xC1,
0xF5-0xFF, and orphaned multibyte leads) the raw 1, but toString('utf8')
renders each as U+FFFD (3 serialized bytes). A b"\xff" flood was undercounted
threefold, so the residual grew to a full budget's worth of raw bytes before
flushing and, near a large maxLogBytes, expanded toward a ~1 GiB peak in the
flush's concat plus toString. Replace serializedBufferCost with accrueStrayCost,
a cross-chunk UTF-8 walker that charges each byte its decoded serialized width;
carry its sequence state on each StrayBuffer.
The child _LogStream had the same-family bug: its early-flush trigger compared
_pending_chars (character count) against remaining (a serialized-byte budget),
so a 30M-NUL newline-free flood stayed under a 50 MB char trigger yet encoded to
~180 MB at settlement, breaching RLIMIT_AS as worker-exit. Track _pending_cost
via the _JSON_BYTE_COST table and trigger on it; keep _pending_chars for the
char-based slice bounds.
Correct the note's surrogate claim (only the string-walking jsonStringCostUpTo
charges a lone surrogate six bytes; the byte walker never sees one). Shrink the
post-truncation fixture below PIPE_BUF for a deterministic single callback. List
the shared stdout/stderr budget as a third honest fail-before exception
(cross-pipe arrival timing is nondeterministic). Add illegal-UTF-8,
broken-multibyte, and child-log-flood regression tests; sync the zh pair.
The stray-sealing regression test asserted copied < 2 MiB — about 4x the
defended sealed shape, so reverting the seal to a re-merge (or removing it)
left the test green. Measured both shapes as the fd-3 sibling does: the sealed
shape copies ~120 KB, the re-merge shape ~538 KB. Tighten the bound to 256 KiB,
which sits between them, and record the measurements in the comment and the
Agent Note so the fail-before claim holds.
stdout and stderr each checked their pending serialized cost against the full
logBudget independently, so both could retain nearly a budget's worth of
newline-free residual at once — double the intended peak, up to ~512 MiB near
the ceiling. The flush threshold now reads the COMBINED cost of both pipes and
flushes both when it crosses, since they share one ledger.
Remove the post-truncation admit() v8-ignore: captureStray's per-line loop
makes that branch deterministically reachable within one data callback (a chunk
whose first newline-terminated line exhausts the budget hits it on the second),
so it is measured by a new regression test rather than ignored.
Refresh two stray-output test comments that still named the removed
StringDecoder; the raw-chunk buffer reassembles a split multibyte sequence by
concatenating before it decodes, and the end flush renders a stranded partial
as U+FFFD via toString('utf8').
Three follow-ups the review caught in the stray-capture rewrite, plus a cost
undercount shared with the log ledger.
Seal the stray fragment list into blocks past MAX_PENDING_CHUNKS, mirroring the
fd-3 reader: a program pacing single-byte os.write(1, ...) calls otherwise
accumulates one live Buffer per write, and the per-object overhead no byte
count sees exhausts the host heap far below the budget.
Flush the residual by its running SERIALIZED cost (serializedBufferCost, a
per-byte lower bound) rather than raw byte count: a control-char-dense
newline-free flood serializes several-fold, so a raw-byte threshold let it grow
to a full budget's worth of raw bytes — up to ~6x what the ledger admits —
before flushStray concat/decoded the whole ~256 MiB residual at once.
Charge a lone surrogate its full six escaped bytes (\uXXXX under ES2019
well-formed JSON.stringify) in both jsonStringCostUpTo and serializedBufferCost,
not the three bytes Buffer.byteLength reports for U+FFFD: a forged log frame
flooding \ud800 escapes was undercharged by half and admitted ~2x maxLogBytes.
Key the sync-spawn leak assertion off the exact bootstrap path from the mocked
spawn's argv, immune to a sibling worker's concurrent staging. Refresh the
stale load-check comment that named the replaced JSON.stringify mechanism.
Add lone-surrogate, stray-sealing, and companion regression tests (per-file
100% coverage); update the Agent Note and zh pair.
The line-aggregating stray capture from the previous round regressed three
ways the review caught. Rewrite it on the fd-3 reader's raw-Buffer-chunk
shape: accumulate chunks with a byte counter and split on the raw 0x0a byte,
so a large newline-free write no longer re-copies the residual and re-scans
from index 0 per chunk (both O(N^2)). Meter each admitted entry by serialized
cost through a new jsonStringCostUpTo that walks to the cap and stops, so a
near-budget control-char-dense line never allocates the sixfold-inflated
JSON.stringify result the old ledger did (the critical: ~1.6 GiB transient
under a large maxLogBytes). Flush the residual explicitly in the closeDeadline
handler before it destroys the streams, so a setsid escapee's path (which
fires no end) does not drop a leader's final newline-free diagnostic.
Harden the sync-spawn leak assertion to a set difference against a pre-run
snapshot, immune to a parallel worker's concurrent tmpdir create/delete.
Decline the round-2 request to enforce the fd-3 ceiling per-frame: the counter
check must precede Buffer.concat to prevent ~2x memory doubling (two
regression tests assert this), and the batch-edge false reject it would fix is
reachable only at a maxLogBytes/maxValueBytes configured within one pipe read
of the 256 MiB ceiling, far past the defaults. Documented at the check and in
the note Alternatives.
Add flood, NUL-flood, short-escape, and closeDeadline-flush regression tests
(restoring per-file 100% coverage); update the Agent Note and zh pair.
The line-aggregating stray capture added two branches — the post-truncation
early return and the residual-overflow admit — that the aggregation and
split tests did not exercise, so per-file coverage dropped below 100%. A
2 MB newline-free native write under a 4 KiB maxLogBytes drives the residual
across the budget (admit-and-truncate) and then short-circuits later chunks,
asserting the captured output ends at the truncation marker and stays under
budget rather than buffering the whole flood.
Wrap spawn and the fd-3 narrowing so a synchronous throw (ENAMETOOLONG on
an over-PATH_MAX pythonBin, EMFILE) removes the run's staging directory and
resolves the same worker-exit class as the async error event, instead of
rejecting run() and leaking the directory.
Aggregate native stdout/stderr by real newline rather than by Node data
chunk: logs entries are joined with "\n" downstream, so a newline-free
write larger than one pipe read no longer reads back with spurious breaks.
The ledger still bounds a newline-free flood.
Track a running scan offset in both frame readers so a large frame
accumulated across chunks is scanned once, not re-scanned from 0 per chunk.
Reword the deadline hard-bound v8-ignore to state its real environment
dependence (PID-1-doesn't-reap container, zombie survivor) and cross-ref
the note's rejected signal-0 alternative; fix settle comments that quoted
the pre-qualification teardown contract; document the capMessage vs
_cap_message billing split on both sides; guard the dispose-after-resolve
heartbeat assertion against a vacuous 0===0 pass; reuse
_TRUNCATION_MARKER_BYTES; note the abandoned-call pending-entry bound.
Update the Agent Note Decision/Testing/Alternatives/Consequences for the
above and record the confirmed-empty finalize as a second honest
fail-before exception; sync the zh pair.
The reap-poll deadline arm sent SIGKILL then finalized immediately, declaring
quiescence on mere signal delivery while the group was still dying. It now keeps
polling for the group to actually empty (bounded by one more reap margin) after
its self-sent SIGKILL, so `finished` resolves only on a confirmed-empty group.
ProtocolChannel.read_frame read the boot/run handshake frames through
FileIO.readline() on the unbuffered fd — one os.read(1) per byte, so a
multi-megabyte program burned CPU (RLIMIT_CPU already in force for the run frame)
in millions of syscalls before ast.parse. It now reads in chunks into the same
_pending buffer the async reader uses; the wrapping os.fdopen is gone. read_frame
is this PR's own code (e7f22ed3), not the protocol layer. The chunked read is a
syscall-count improvement with no cross-platform-deterministic failure to assert,
noted as such in the Agent Note.
The closed-loop reply-pump guard now ships with a deterministic regression test:
a worker thread abandons a binding so its loop closes, the host answers that call
before a later binding, and the pump must survive the closed-loop
call_soon_threadsafe to deliver the later reply (host-gated ordering makes it
deterministic; unguarding the pump hangs the later binding to the wall clock).
Align the quiescence self-description with the shipped setsid limitation:
teardown()'s JSDoc and the Agent Note's Problem line now qualify "no subprocess
outlives the fiber" to subprocesses that stay in the child's process group, with
a setsid()-escape exception pointing at the README. Tighten the setsid-orphan
fixture's self-timeout to 5s and its upper-bound assertion to <4000ms so a failed
deadline backstop is a sharper red. Register the new regression tests in the note.
The child reads these byte budgets through int(...), which silently floors a
float, so maxLogBytes: 3.5 would truncate at 3 bytes child-side while the host
meters and marks at 3.5 — the two sides enforcing different public config. Gate
them to integers at load, as the worker backend does; correct the stale comment
that claimed the int()-truncated caps needed no gate. Adds a regression test.
The SIGXCPU timeout message changed from "CPU budget (Ns) exhausted" to name the
configured value as a ceiling; two existing timeout tests asserted the old text.
Assert "CPU time exhausted" to match.
Raising maxValueBytes' load bound to ceiling-envelope assumed both budgets are
metered in serialized (JSON-escaped) bytes, which held for completion values and
logs but not the diagnostic: _cap_message capped by raw UTF-8, so a control-heavy
message near maxValueBytes could serialize sixfold and breach the fd-3 frame
ceiling — the silent worker-exit inversion the load check prevents. _cap_message
now accumulates per-byte serialized cost (new _JSON_BYTE_COST table) and cuts the
prefix that fits. Also reword the host SIGXCPU timeout message to name cpuSeconds
as the configured ceiling rather than a budget a stricter inherited RLIMIT_CPU
soft may undercut. Adds a control-heavy-diagnostic regression test.
The group-reap poll folded its deadline arm into the empty-group arm, so a host
event loop blocked past graceMs + CLOSE_REAP_MARGIN_MS would run the overdue
poll before the grace SIGKILL timer: the group is still non-empty, the deadline
has passed, and the shared arm cancelled the never-fired SIGKILL and finalized —
releasing a SIGTERM-ignoring same-group survivor for good. Split the arms: empty
group cancels the moot timer and finalizes; deadline-with-non-empty-group sends
SIGKILL itself (idempotent if the timer already ran) before finalizing. Adds a
regression test that busy-blocks the loop past both timers and asserts the
survivor's heartbeat freezes.
The group-reap poll's deadline arm (Date.now() >= deadline) is a backstop that
SIGKILL emptying the reachable group never reaches, leaving one uncovered branch
under the per-file 100% gate. Mark it v8-ignore with the reason and drop the
always-true graceTimer-defined guard inside pollGroup (it runs only when killing
is set, so kill() has armed the timer).
settle() dropped the run from `live` eagerly, before the grace-window SIGKILL
reaped a same-group survivor. A dispose() racing a just-resolved run() then
snapshotted an empty `live` and returned while the descendant was still alive,
so teardown's "no subprocess outlives the fiber" (and its JSDoc) was false for
that window. The run now stays in `live` until the process-group poll confirms
the group empty, at which point it is both dropped from `live` and its finished
promise resolved. Adds a regression test asserting dispose() of a completed run
with a same-group survivor returns only after the survivor stops executing.
The class docstring still credited the GIL plus per-frame PIPE_BUF atomicity for
serializing writes, which _write_lock's full-write loop already superseded. State
the current contract (writers serialized by _write_lock around a full-write loop)
and drop the double blank line under the binding-replies note heading.
Record that die_if_cpu_exhausted compares against the effective clamped cpu_soft
in the rlimit section, and add the recheck-timeout test to Testing; re-record pair.
The settlement-time CPU recheck compared spent CPU against the configured
cpuSeconds, but _clamped may have lowered the effective soft limit to a stricter
inherited value. A program that traps SIGXCPU, burns past the inherited soft,
and returns inside the soft-to-hard gap was checked against the configured value
and falsely reported successful, bypassing the inherited limit. The recheck now
uses the clamped cpu_soft. Adds a regression test that inherits a 1s soft CPU
limit and asserts a SIGXCPU-trapping over-burn is a timeout, not a success.
Record the call_soon_threadsafe-onto-a-closed-loop guard in the binding-reply
section of the settlement-fixes Agent Note; re-record the bilingual pair.
A binding called from a worker thread records that thread's loop for its reply.
If the thread finished and closed its loop before the host reply arrived,
_pump_replies' call_soon_threadsafe onto the closed loop raises RuntimeError;
unguarded, that ends the pump task and strands every later reply. Wrap the
schedule in a try/except that drops the moot reply (nothing awaits it) and keeps
the pump serving.
A descendant that calls setsid()/start_new_session leaves the child's process
group, so kill(-pid) teardown cannot reach it; if it also releases the inherited
pipes the run still settles and the fiber goes quiescent while the orphan runs.
This is the containment boundary (model code has bash-equivalent trust), not a
guarantee; reaching such an orphan needs descendant-pid tracking and is deferred.
Two further review findings on the CPython backend:
- The grace-window SIGKILL timer was left armed after settlement, so on a
normal completion a kill(-pid) could fire up to graceMs later and strike a
recycled pgid once the kernel reused the leader's pid. settle() now clears
the timer the moment the process group is confirmed empty (the normal path
and when the poll sees the survivor gone), bounding the reuse window to the
genuine-survivor case where the group cannot be empty to reuse.
- _clamped bounded rlimits by the inherited hard limit only, silently raising
an inherited soft limit stricter than the request (loosening RLIMIT_AS or
deferring RLIMIT_CPU SIGXCPU). It now clamps each side against its own
inherited counterpart and pins soft under hard, keeping the strictest of
configured and inherited. Adds an inherited-soft-limit regression test.
Agent Note expanded to seven fixes with the two new rejected alternatives;
zh pair re-recorded.
Two review findings on the CPython backend:
- Disposal could return while a same-group descendant that ignores SIGTERM
but releases the inherited pipes was still alive: the leader's close fired
and the previous fix relied on an unref'd SIGKILL timer that a short-lived
host never fires, reparenting the survivor to init. settle() now withholds
the run's finished promise on a ref'd process-group poll until the SIGKILL
has emptied the group (bounded by graceMs + margin, zero-cost when already
empty), so teardown's "await each child's exit" holds.
- A binding called from a model worker thread via asyncio.run created its
reply Future on that thread's loop, but _pump_replies completed it directly
from the main loop; asyncio.Future is not thread-safe across loops, so the
call hung to the wall clock. Replies now complete via the owning loop's
call_soon_threadsafe, and a lock serializes the id claim/write/advance.
Tests: the same-group reap case now asserts a heartbeat file stops (robust
whether the killed descendant is reaped or a zombie, so it holds where PID 1
does not wait() orphans); a cross-loop case runs a binding from a worker
thread and asserts the reply round-trips instead of timing out. Agent Note
expanded to all six fixes with rejected alternatives; zh pair re-recorded.
The frame-ceiling cap test asserted the old (ceiling-envelope)/6 bound and
its 44739232 message. The load bound is now ceiling-envelope because both
budgets are metered in already-escaped bytes; assert 268435392.
Address review findings on the CPython backend:
- CRITICAL: a model program could leave a descendant in the child's own
process group that ignores SIGTERM but releases the inherited pipes, so
the leader's `close` fired and settle() cancelled the pending SIGKILL
before it escalated — run()/dispose() returned while that child lived.
kill() now unrefs the grace timer and settle() no longer clears it, so
the SIGKILL reaches the whole group; killGroup swallows ESRCH when the
group is already gone (the normal case). Adds a real-subprocess
regression test.
- WARNING: the maxLogBytes/maxValueBytes load bound divided the frame
ceiling by 6 for escape expansion, but both budgets are metered in
already-escaped serialized bytes, so a payload occupies at most
cap+envelope on the wire. Bound is now ceiling-envelope; drop the unused
escape constant.
- Narrow the runtime.spec.ts header to "no subprocess mocks" (it mocks
node:fs.copyFileSync for staging-failure cases).
- Use full-width punctuation in the README.zh.md prose per translation
rules; re-record the pair.
The package README (both languages) still described this layer as
protocol-only with the PythonCodeRuntime implementation deferred to a
later PR, contradicting the shipped code. Rewrite the intro to describe
the registered runtime, add a Configuration section for every Config cap,
and drop the "implementation not in this layer" limitation. Also pin the
residual-detach fixture's size invariant: the byteLength assertion only
holds above Node's Buffer pool threshold.
The manifest omitted @deepseek-ai/dsh-code-runtime although src/index.ts
imports CodeRuntime and the portable-identifier constants from it and the
tsconfig references ../code-runtime. A three-way package.json merge over
the protocol-layer stub dropped the entry; restore it in peer and dev
dependencies so the declaration matches the import.
Unwrap the English note to one physical line per paragraph (verify-md-wrap)
and retarget the backend link to the fd-3 protocol architecture note that
this stack actually ships (verify-md-links); re-record the bilingual pair.
Land the PythonCodeRuntime implementation on top of the fd-3 protocol
seam: python3 -I per run, binding namespace over fd 3, RLIMIT_CPU/AS,
wall-clock timer, and SIGTERM->grace->SIGKILL process-group teardown,
with the real-subprocess integration suite.
Fixes three defects surfaced on the source PR's review before they ship:
- boot-write failure resolved a worker-exit through finish()/settle()
that read wallTimer/onAbort/live in their TDZ, rejecting run() instead;
the boot write now runs after those bindings and the v8-ignore that hid
the branch is removed.
- log capture serialized against settlement with no lock while model
daemon threads keep writing; LogBuffer now owns one shared re-entrant
lock taken by write/flush_line/push.
- the fd-3 line residual was a subarray view pinning the whole joined
frame; it is copied into a right-sized Buffer via detachResidual so
pendingBytes measures what is retained.
Resolve the 08-08 note conflict (keep the zero-build rewrite, drop the
SQLite busy-journal sentence removed with the SQLite backend) and drop the
now-removed session-persistence-sqlite built-package suite from the
lib-consuming self-skip inventory in the ci.yml comment, the
ci-workflow.spec.ts comment, and the partitioned-coverage note; the
inventory is now image-loadable, transform-corpus, and client-bundle.
`AGENTS.md` sat at exactly its 1950-word ceiling on master, so the one line this
branch adds to the repository layout — a new top-level package group — cannot
fit at any length: even a one-word description overflows. The description is
condensed to five words and the ceiling raised by ten, the smallest change that
keeps every package group listed. Omitting only `net/` from a list that names
every other group was the alternative.
One new test drove an IPv4-mapped literal through a full fetch. An IPv6 literal
sends `resolvePublicAddresses` looking for a NAT64 prefix before it refuses
anything, and that is a real DNS query — 5s under load, 6ms here, which is why
it passed alone and timed out in the full suite. The three IPv4 cases already
prove the branch end to end without touching the network, so the mapped form is
asserted on the predicate instead.
cache.spec.ts polled fire-and-forget fail-soft writes with a fixed 40ms
settle(); contended runners drain the write after the window, so the
warn/row assertions flaked. Poll the observable outcome with vi.waitFor
(5s) instead, matching the file's existing cold-read write-back pattern.
The sdk-client and subagent-dsh-sdk dispose-ladder tests passed tight
confirmation budgets (disposeGraceMs 100-300ms) to real children; on a
contended runner the SIGKILL exit edge can arrive after the budget and
close() misreports a slow reap as failure. Use the product-default
budgets (disposeGraceMs 3000ms) for the real-child cases; the fake-child
negative cases in dispose.spec.ts keep the 10ms bound.
client-bundle.client.spec.ts reads packages/client/ui-trajectory/lib/client.js
and skips all three cases when the bundle is absent, so it is a fourth
lib-consuming suite in the instrumented corpus. List it beside built-package
in the ci.yml comment, the ci-workflow.spec.ts comment, and the
partitioned-coverage note (both languages).
Both coverage lanes now run before any build, so the post-build
uninstrumented gate wording applies only to the serial-windows complete
reference; preview builds still exercise the suite against real
artifacts.
Strengthen the ci-workflow.spec.ts guard to match any 'pnpm run build'
spelling (corepack prefix, multi-line run blocks) instead of one exact
string, and complete the lib-consuming self-skip inventory with the
webworker-runtime transform-corpus import sweep alongside the packer
image assertions and the built-package check. Update the ci.yml comment
and the partitioned-coverage note (both languages) to match, and drop the
stale 'post-build' phase wording and the native-Windows build-wait
rationale from the coverage-exempt comment.
Review found `127.0.0.2` routed through the proxy. The bypass list carries four
literal loopback entries because that is all a consumer reading an environment
can match, and `proxyForUrl` matched only those — leaving the rest of
`127.0.0.0/8`, `0.0.0.0`, and the IPv4-mapped spellings routed through a proxy
that could then reach them. Loopback is now recognised structurally, which a
list entry cannot express; the published entries stay for the environment
readers.
The same review case exposed a wider one. `web_fetch` skips its address checks
on a proxied hop, because the proxy resolves the origin — but a literal needs no
resolution, so the skip bought nothing and let a proxy on this machine reach
every private range those checks refuse, `169.254.169.254` included. A literal
the checks would refuse now takes the validated path, where the existing
refusal already covers it.
Tests that proved a tunnelled hop used a loopback origin, which no policy can
route through a proxy any more. They name a host only the proxy can answer for
instead — closer to what a proxied request actually looks like.
The user guide promised the proxy carried every outbound request including
telemetry. It carries neither on an older Node, nor anything a model-authored
script sends, so the promise is narrowed and the exceptions listed. A password
in a proxy URL reaching every tool DSH runs is documented there too: it is how
the variable already behaves, and worth knowing before putting one in.
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 windows-coverage job built the workspace before running the same
ci-coverage gates as Linux, but the instrumented corpus resolves
workspace imports to src through the tsconfig paths map and never
consumes lib/; the two lib-consuming suites (webworker-packer
image-loadable, session-persistence-sqlite built-package) self-skip on
unbuilt checkouts, exactly how the Linux lane already runs them. Remove
the build step so both lanes behave identically, and pin the zero-build
invariant in ci-workflow.spec.ts (red before this change, green after).
Agent Notes updated in place: corrected the build-wait rationale and the
wrong attribution of the packer assertions to the instrumented suite.
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.
pnpm hardlinks node_modules files to the store on the same volume, and
TypeScript's native realpath resolves those links back to store paths
(F:/.pnpm-store/v11/files/...), producing TS6231 during tsc -b and vite
resolution. ReFS block cloning (package-import-method=clone) gives each
file an independent path while sharing physical blocks, avoiding the
leak without the copy cost. Clone mode needs the @reflink/reflink native
module, which the system corepack pnpm carries but pnpm/action-setup's
dest build omits, so installs run through corepack pnpm.
The install steps branch on the workspace filesystem: clone only on
ReFS, plain install on hosted NTFS (which rejects copy-on-write). The
serial-windows store points at F:\.pnpm-store to share the ReFS volume.
Agent Note 2026-08-30-windows-refs-store-block-clone-install records the
rationale; ci-workflow.spec asserts the branch.
A settled top-level `read_image` call printed its raw attachment object as
literal text in the tool card — `{"type":"image","attachment":{…}}` —
instead of the image, because no presentation metadata told a client card
how to present the reference and the tool-card layer had no image concept.
Host: `read_image` declares an `output.presentationMeta` persisting
`{ path }` only. The attachment reference deliberately lives in the
settled result content — the single record a `tools/post-execute`
replacement rewrites — not in `meta`; the id is opaque and
provider-owned, checked for existence only.
Client: `imageCardModel` derives the card from the call head, the meta
path, the result's own image block, and a shape-matched envelope. ToolRow
gains an `image` card slot; the `read_image` toolview declares the
Tool-owned `tool.call.images` slot as its child and dispatches the
gallery through it. ui-chat down-threads the session-authorized loader
(`ChatNodeOwnerProps.loadImage`), so the tool layer supplies only derived
references plus the loader and never imports an attachment
implementation; ui-attachment fills the slot with its message gallery
renderer. The card keeps the envelope text below the gallery for the
no-attachment-plugin deployment. An image-bearing tool registers a keyed
toolview; the generic fallback keeps its flattened text. `read_image`
joins the read variant with its own locale title key; both rows share
`read-family-row.tsx`.
Verification: `read-image.spec.ts` (metadata projection, envelope by
shape, reference narrowing, real-execution round trip, rejection
branches incl. non-digest ids), `image-card.client.spec.tsx` (derivation,
row render site dispatching the slot, keyed registration with the
child-slot declaration, empty-slot fallbacks, media-type enum), keyless
snapshots (`read-image-gif` added; read-image/-dimension/-reencode
updated to the `{path}` meta), five injected-defect negative controls,
and a demo GIF recorded from this PR's head through the official
image-capable model.
The Windows coverage lane failed on four cases that assume `http_proxy` and
`HTTP_PROXY` are separate variables. They are one variable there: `process.env`
is case-insensitive, and the launch snapshot folds names for the same reason. A
scenario built on "the user set only the lowercase name" cannot exist.
Two of them assert a contract rather than a spelling, so they now hold either
way: what reaches a child for a scheme the user named is the user's own value
and never the derived one, and a nested install carries the active policy's
value rather than the outer install's published one. Both read over the pair of
names instead of one.
The other two are about the case distinction itself. Neither can hold on a
folded environment, so each asserts what that platform does instead of skipping:
the later entry wins where a preference cannot be expressed, and a diagnostic
names the spelling resolution asked for. Both were verified against the folding
arm rather than reasoned about.
A developer's Clash and a CI runner's squid both export HTTP_PROXY and its
siblings. Now that the harness honors them, an ambient value decides test
outcomes: this PR has already recorded a proxy's 502 page as a snapshot's
expected output, and let a runner's own export stand in for "what the user
exported" in an assertion about inherited names.
A Vitest setup file clears the proxy names before any suite runs, wired into
every configuration that declares a setup. Real-API e2e is cleared too: before
proxy support existed every request connected directly and that suite passed, so
direct is the environment it is known to work in.
`NODE_USE_ENV_PROXY` cannot be cleared this way — Node samples the proxy
environment at process start — and the module says so. A proxy application never
exports it, and the eight names one does export are fully handled: with all of
them set, the affected suites pass.
The wiring is what regresses, so that is what the test pins. Configurations are
discovered rather than listed, because the web suites carry no setup today and a
hand-written list would let one of them gain a setup without gaining this one.
`plugin.spec.ts` kept its own copy of the eight names to guard against the
machine; it never sets a proxy variable itself, so the setup replaces that
entirely. `install.spec.ts` had one assertion waiting on a DNS miss with no
deadline, which timed out once under load.
`installGlobalProxy` claimed each worker thread calls it with a policy its host
passed through `workerData`. Nothing does: the two workers this repository ships
evaluate model-authored scripts and are deliberately left without a proxy URL
that may carry credentials.
`agentBindings` now walks the whole tree to reach a dynamic `import('undici')`,
which pushed the repository-wide scan past the 5s default under the coverage
lane's instrumentation.
Both violations name one of two words in source: an agent construction needs a
binding from the undici module, and the option is a property called
`dispatcher`. Skipping a file that mentions neither leaves 21 of 1597 files to
parse, so the scan runs in ~60ms instead of ~500ms — well clear of the timeout
even instrumented.
Second review pass on the outbound proxy work.
The installed dispatcher was undici's EnvHttpProxyAgent, which reuses the HTTP
proxy for `https:` whenever no HTTPS proxy is present. That is exactly the state
this package resolves after refusing a SOCKS or malformed URL the user named for
`https:`, so the scheme the diagnostic reported as direct was tunnelled anyway.
The dispatcher is now an Agent whose per-origin factory calls `proxyForUrl`, so
routing and `proxyForUrl` cannot disagree by parsing the same list twice.
`childProxyEnv` returned only the names the user exported, which left a child
Node direct whenever the proxy came from `ALL_PROXY` or from cordis.yml — Node's
`NODE_USE_ENV_PROXY` reads neither — and stripped the merged loopback bypass so
the child sent its own localhost traffic to the proxy. A scheme the user named
in either casing still reaches the child exactly as written; one they named in
neither now carries the resolved value, and the bypass list is always the merged
one.
A nested install (the plugin mounted over the launcher's policy) recorded the
outer policy's published values as the user's, then cleared the record on
disposal, so every later child inherited the normalization instead. The record
now belongs to the outermost install and is restored, not dropped.
`web_fetch` read the active policy twice — once to skip address pinning, again
inside the transport — so a disposal landing between the two reads produced an
unpinned direct connection to a host nothing validated. One snapshot now decides
both.
Also: the node:http proxy test asserted a route the engines range does not always
have, and the gate could not see undici bound through `await import('undici')`,
the form this repository actually uses.
The case builds a user environment and asserts a child receives it verbatim,
so a runner that exports its own proxy supplied half the "user" values and
decided the assertion.
The workflow worker no longer receives proxy configuration: it executes the
model-authored script body, and a proxy URL may carry credentials. A child
process now inherits the values the user exported rather than this process's
normalization, so a SOCKS proxy set for curl survives and no HTTPS_PROXY is
invented. `mode: 'off'` installs a direct dispatcher instead of recording a
policy the global dispatcher ignores, and the environment snapshot is taken
before any write so Windows restores the user's values.
E2B picks its proxy from the control-plane URL the SDK will really call, the
OTLP agent honors `exporter.keepAlive`, and a scheme whose own value was
refused stays direct instead of borrowing another scheme's proxy.
verify-no-bare-dispatcher parses the TypeScript AST as scripts/AGENTS.md
requires; it immediately found the `{ dispatcher }` shorthand the regex missed.
A replay must not depend on the runner's network policy, the same reason it
pins its home and sessions root. The harness now honors the proxy environment,
so a runner exporting one sent the web-fetch scenario's fixture request to a
proxy that could not resolve the fixture host and recorded that proxy's error
page. Clear the proxy names in both test-support spawners, from the one list
dsh-http-proxy owns.
Both adapters' inference requests were argued from a code read and, for
DeepSeek, from one product smoke. Drive each shipping adapter at an
unresolvable endpoint through a fake proxy instead, and cover pi-ai's provider
stream rather than only its model discovery.
The VFS packer sweeps module requests statically, so dsh-http-proxy's agent
factory made the preview image unpackable: it names node:https for the SDKs
that post through Node's core HTTP modules, a path the worker never takes.
Mock it the way node:net is mocked rather than hiding the request.
NODE_USE_ENV_PROXY reaches Node 24.0+ and 22.21+, while engines admits 22.19.
Assert the direct connection on an older runtime instead of only the proxied
one, so the seam is executable rather than prose.
Node's built-in fetch ignores HTTP_PROXY, so every harness request connected
directly regardless of what the user exported. Resolve one policy from the
launch environment and install it as undici's global dispatcher, then wire the
four surfaces a global dispatcher cannot reach: web_fetch's pinned transport,
the OTLP exporter's node:http agent, the E2B SDK's own proxy option, and the
environment a child process or worker thread is given.
Each outbound call site carries an egress test that drives its real code path
through a fake proxy; that measurement is what found the OTLP and E2B gaps.
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.
A steer or follow-up accepted while a turn is closing is now claimed by a
fresh turn at the driver's clean exit instead of stranding in the inbox;
cancellation and pre-step rejection still park accepted work. Continuable
subagent follow-ups accept image parts: the wire is upload-shaped, the Host
admits and persists each batch before inbox acceptance, and delivery is
refused when the child model declines image input. The queue dock renders
durable image thumbnails instead of an [image] text marker.
Fixes#3186
- 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.
The native Windows containment tests expect the stdout stream to end
shortly after the direct target exits, but 1s was too tight under the
Windows CI runner's cold-start and process-teardown jitter. Use a 5s
bounded race so the assertion still proves EOF eventually arrives
without flaking on slow runner startup.
@@ -16,11 +16,11 @@ 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
Each Agent Note belongs to one path-encoded class from the closed set in `scripts/agent-note-tree.ts`; the classification gate rejects other folders. Adding a class requires updating the canonical set and this section. See the [classification Agent Note](implemented/process/2026-06-20-agent-note-classification.md).
Each Agent Note belongs to one path-encoded class from the closed set in `scripts/agent-note-tree.ts`; the classification gate rejects other folders. Adding a class requires updating the canonical set and this section.
| Class | What it covers |
|---|---|
@@ -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.
## 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.
English | [中文](2026-06-18-shared-persistence-write-coordinator.zh.md)
## Problem
The JSONL provider needs correctness-heavy write orchestration around its storage primitives: per-Session state, `session/created` adoption, prefix reads, write-behind control, per-id operation serialization, HMR seeding, and dispose drains. Keeping that lifecycle in the Service Definition prevents an out-of-tree provider from copying it. The removed first-party database provider demonstrated the duplication cost; the [JSONL-only persistence decision](../simplification/2026-08-30-jsonl-only-session-persistence.md) owns its removal.
## Decision
`dsh-session-persistence` exports a backend-agnostic `PersistenceCoordinator`. The JSONL provider composes one (`new PersistenceCoordinator(ctx, this)`), implements the small `PersistenceBackend` hook interface, and delegates its stateful public methods (`create`/`append`/`prepare`/`load`/`inspect`/`readFrom`) to it. Backend-owned metadata and revision listing bypass the coordinator.
Composition, not inheritance. The coordinator is a concrete class the backend holds, not a base class the backend extends. The risk that a coordinator makes unusual backends fight an inheritance hierarchy is avoided: a backend exposes only the hooks and cannot reach the coordinator's private orchestration state. A third-party backend MAY still implement the abstract service directly without the coordinator, including immutable logical inspection and the default preparation fallback through `load`.
The coordinator holds one lifecycle entry for each exact live `Session`: initialization plus a package-private write controller that owns pending events, a fixed batching deadline, the active write, failure retention, and the shared flush barrier. Each `session/event` enters that bounded write path, and `session/flush` bypasses the wait to observe quiescence. The [flush-controller simplification](../simplification/2026-07-23-collapse-persistence-flush-state.md) owns controller consolidation; the [bounded batching decision](2026-08-08-bounded-session-persistence-write-batching.md) owns scheduling cadence.
Creation borrows the exact `Session.events` snapshot as its persistence seed. `Session` has already detached, validated, and deeply frozen every event, and the snapshot array remains stable when later appends replace the cached view. The coordinator and its backend hooks only read this typed in-process value, so cloning the complete log again would duplicate the ownership work described by the [agent-scope runtime decision](2026-07-12-agent-scope-runtime-design.md#session-append-materialize-validate-commit-notify). Public persistence `append()` still snapshots caller-owned input at its API boundary.
Prepared-session suffixes and events admitted to the write-behind queue retain their existing copies. Those paths establish asynchronous queue ownership one suffix or event at a time and have no measured whole-log clone cost; removing their copies remains a separate ownership audit rather than part of creation-seed borrowing.
The coordinator retires a session from `session/disposed`: it waits for the controller's initialization and current flush, serializes a final drain, and removes the controller and owned per-id state only after success. A failure leaves the controller discoverable for backend teardown to retry. Settled per-id chain tails remove themselves only when they are still current, so a completion cannot erase a newer operation for the same id. Backend teardown unregisters write-path listeners, flushes every remaining controller, awaits per-id operations, and then closes the backend.
### The hook interface (`PersistenceBackend<TornMarker>`)
Five required members plus optional empty-materialization and lifecycle hooks form the only boundary between the coordinator and storage:
-`name` — backend label for the dispose-failure `AggregateError`.
-`loadStored(id)` — read one stored prefix by id across every storage scope. Preparation, logical load/inspection, physical suffix reads, live adoption, and the create-collision probe share this lookup. The coordinator asserts the returned id and rejects a stored/live cwd mismatch before repair or state publication.
-`appendBatch(meta, events, isMaterialized)` — durably append a contiguous batch, lazily materializing the session ATOMICALLY when not yet materialized. Ordinary creation therefore cannot leave an abandoned materialized-but-empty session.
-`materializeHeader?(meta)` — explicitly persist a header-only session for `SessionPersistence.ensureMaterialized(session)`. This is reserved for a lifecycle frontend that treats an empty session itself as a resumable durable resource; [standard ACP automation controls](../feature/2026-08-22-standard-acp-automation-controls.md) are the first consumer. Backends that support that lifecycle implement the hook; lazy creation remains the default.
-`commitRepair(meta, tornMarker, closers)` — make a crash repair durable: truncate the torn tail (iff `tornMarker !== undefined`) and append `closers`. **NOT required to be atomic** — JSONL legitimately truncates then appends in two fsync'd steps. Used by `prepare`/`load` (truncate + synthetic closers) and live adoption (truncate only, `closers = []`).
-`list()` — list all stored metadata.
-`close?()` — optional lifecycle teardown for a provider with owned resources; JSONL omits it. The dispose effect awaits it after the quiescence drain so a close failure never masks a drain error.
### The opaque torn marker
The single design choice that keeps the seam clean: the crash-repair "where is the torn tail" token is opaque to the coordinator. The coordinator computes the synthetic closers (it owns `interruptedTurnClosers` from `dsh-session`), but it only tests `tornMarker !== undefined` and passes the value straight back to `commitRepair`; it never inspects it. JSONL carries the byte offset to truncate to plus any complete events decoded from an incomplete final frame, while another provider may choose its own marker type. The coordinator therefore knows neither byte lengths nor frame recovery state.
## Testing
The shared `runPersistenceContract` proves that JSONL `inspect` balances an interrupted logical view without changing storage or revisions before `prepare` or `load` commits recovery. `runCoordinatorContract` (`tests/coordinator-contract.ts`) covers adoption, HMR, collision, Session and provider disposal drains, and crash-tail repair through an in-memory reference and JSONL. `persistence.spec.ts`, `preparations.spec.ts`, and `write-behind.spec.ts` cover preparation reuse and reservation, bounded prepared-state eviction, fixed-window follow-up batches, live-controller cleanup, same-id chain-tail races, failed-batch retry, and close ordering. JSONL specs retain storage mechanics and the through-coordinator torn-tail case that exercises the opaque-marker branch.
## Alternatives considered
- **A base class the backends extend** — rejected for composition: a backend exposes only the hooks, cannot reach the coordinator's private orchestration state, and a third-party backend may still implement the abstract service directly without the coordinator at all.
- **A wider hook API** — each candidate hook folds away: there is no scope-specific live lookup because `loadStored` plus the coordinator's cwd check preserves the collision boundary, no storage-locator generic because validated JSONL metadata reproduces its path, no separate `materialize` hook because the first batch must commit atomically with materialization, no separate create-collision probe because it is `loadStored(id) !== undefined`, and no coordinator pass-through for `list()` because listing needs none of the orchestration.
## Consequences
The coordinator adds one indirection, an opaque torn marker, detached Session-retirement tasks, and bounded prepared Session state, but centralizes correctness-heavy orchestration for the JSONL provider and future implementations. Session disposal remains an observe-only event, so the Session owner does not await persistence retirement; the coordinator contains failures, preserves pending events in the live controller, and makes provider teardown the quiescence boundary. Its hook surface stays narrow: identity, adoption, collision checks, preparation, and immutable inspection reuse `loadStored`; materialization stays atomic inside `appendBatch`; and listing bypasses the coordinator. Read models use `inspect` rather than `load`, so observing a persisted open turn does not commit interruption closers; the [Session preparation decision](2026-08-05-session-preparation.md) owns reuse, reservation, and publication. A new provider implements storage primitives rather than copy the bounded write lifecycle.
协调器通过 `session/disposed` 退役会话:它等待控制器完成初始化和当前 flush,串行执行最后一次排空,且仅在成功后才移除控制器与其拥有的每 id 状态。失败时保持控制器可被找到,以供后端 teardown(拆除)重试。每个 id 的已结算链尾仅在其仍是当前链尾时才移除自身,因此旧操作完成后不会抹除同一 id 的新操作。后端 teardown 会注销写入路径监听器、flush 每个剩余的控制器、等待所有按 id 串行化的操作,最后关闭后端。
### 钩子接口(`PersistenceBackend<TornMarker>`)
五个必需成员加可选的空会话实体化与生命周期钩子,构成协调器与存储之间唯一的边界:
-`name`——后端标签,用于 dispose 失败时的 `AggregateError`。
-`loadStored(id)`——按 id 跨所有存储范围读取一个已存储前缀。准备、逻辑加载/检查、物理后缀读取、存活会话接管与创建碰撞探测共用此查找。协调器会断言返回的 id,并在修复或发布状态之前拒绝已存储记录与存活会话的 cwd 不匹配。
@@ -51,7 +52,7 @@ Kept deliberately narrow per the "not every string needs a brand" policy. Each o
- **`ModelId`** (`GenerateOptions.model`, the `LlmRuntime` adapter-registry key) — a real cross-package lookup key (config → agent → llm → adapter); a reasonable next brand, left out only to keep this decision's blast radius focused.
- **`ToolName`** (the `ToolRuntime` key) — author-defined, human-readable, and rarely confused with another id; the weakest candidate, likely not worth a brand.
- **`ErrorCode`** (`HarnessError.code`) — a closed vocabulary (`ABORTED`, `NO_ADAPTER`, …), not a per-instance id; better served by a string-literal union than a brand, if anything.
- **Numeric ordinals** — turn number, step number, and the event `seq` are `number`, not `string`, so `Branded<string>` does not apply; a parallel `number & { readonly [BRAND]: B }` variant could brand them, but they are positional ordinals rarely passed across boundaries, so the payoff is low.
- **Other numeric ordinals** — the [Session sequence and log-offset decision](2026-08-31-session-sequence-and-log-offset-brands.md) brands event identities and log gaps because they cross persistence and reference seams. Turn and step numbers remain plain numbers: they are payload-local ordinals and are not interchangeable with Session event positions.
- **Validated construction** — `brandString<T>()` performs no runtime check, and every boundary (ACP `sessionId`, provider-issued `call.id`, the empty-string fallback in `dsh-llm-deepseek`) trusts the raw string. A `SessionId.parse()` / `isValid()` companion that throws on malformed input at boundaries is a genuine gap, but it is a runtime-behavior change with its own design (what is "malformed"? what happens on failure?) and belongs in its own decision.
# Agent Note: Package-owned invariant service contract
Status: implemented
Archived: 2026-09-04
English | [中文](2026-07-19-package-owned-invariant-service.zh.md)
## Problem
Runtime invariant checks span session traces, agent state, scoped dispatch, and request reconstruction. Putting all checks in one diagnostics package makes that package import product vocabularies from unrelated domains, centralizes tests away from their owners, and requires the central package to change whenever a product package adds or removes a check.
Deployments that opt into diagnostics need more than presence or absence of one plugin. Such a composition carries the known invariant contributions while permitting a global off switch and package-selective diagnostics. Selection must remain stable when a package loads later or reloads under HMR, and disabled contributions must not allow two plugins to claim the same package name silently.
Published ownership must be mechanically complete. Without a repository rule, a package can expose a partial companion, dependency, or publication map and remain broken until a maintainer notices the gap; packages that publish none must keep their reason reviewable in the README.
## Decision
### One registry service, package-owned contributions
`@deepseek-ai/dsh-invariants` is a product-independent Cordis service plugin that registers `ctx.invariants`. It owns configuration, registration uniqueness, child-fiber lifecycle, and package-attributed failures. It imports no session, agent, scope, or agent-loop package and contains none of their checks.
A workspace package publishes a `./invariant` companion plugin only when it owns an independently observable event or mutable-data relationship. The companion registers its exact full npm name. Packages without such a relationship omit the companion and publication wiring and record the reason in their README; generated placeholders, empty installers, and synthetic API-shape assertions are forbidden by the [runtime-contract Agent Note](2026-07-19-package-invariant-runtime-contracts.md) and [omission decision](../simplification/2026-08-28-omit-unneeded-invariant-companions.md). Package root entrypoints do not import or register diagnostics implicitly, so loading a root package does not change runtime checking or require the invariant service.
### Configuration and selection
```ts
interfaceConfig{
enabled?: boolean
package_allowlist?: string[]
package_blocklist?: string[]
}
```
Defaults are `enabled: true`, `package_allowlist: []`, and `package_blocklist: []`. For a full registration name, selection is:
Blocklist matches override allowlist matches. Each list entry is a case-sensitive JavaScript regex source compiled by `new RegExp(pattern)`. Matching is unanchored unless callers supply `^` and `$`; slash-delimited syntax and flags are not interpreted. Startup rejects blank, whitespace-padded, invalid, or duplicate sources within either list. A source that matches no loaded package remains valid because registration order, later loading, and HMR must not change config validity.
### Registration and failure ownership
The public registration boundary is `ctx.invariants.register(packageName, installer)`. It reserves one active registration per full npm package name even when filters disable installation, and returns the effect disposer. Disposing the companion or service releases the reservation and all contribution state.
An enabled installer runs in a dedicated child Cordis fiber owned by the service. `InvariantInstaller.inject` declares the child fiber's service API explicitly; the registry carries no product-specific dependency metadata. The service joins a returned installer promise before registration succeeds, so asynchronous startup checks remain transactional. The installer receives a bound `fail(message)` reporter. Calling it throws an `Error` subclass named `InvariantError` with stable code `INVARIANT` and the registering `packageName`; it does not extend a product-package error base.
Registration setup is transactional. If an installer fails after registering listeners, the child fiber is disposed completely and the name reservation is released before the failure escapes. Filtered registrations create no child but retain their reservation until disposal. Reloading a companion therefore begins with one clean installer state; stateful contributions rebuild baselines from their owning services.
The former functional-plugin entry point and one-argument `InvariantError` constructor are not retained as compatibility APIs. The repository is pre-release and all call sites move to the service and package-attributed error together.
### Initial stateful companions and exhaustive ownership
| Companion entry | Registration name | Owned checks |
These four owners supplied the initial stateful checks. Later owners add companions for real event or mutable-data relationships, while packages without one omit the companion and document why. Every published companion is a separately bundled `./invariant` export with its own declarations and Loader-safe namespace plugin shape.
`verify-package-invariants` discovers every workspace package, accepts clean omission, and rejects partial companion wiring, generated markers, empty installers, installers that omit or ignore the reporter, foreign or unresolved registration names, missing `./invariant` exports or published files, missing invariant peer/development dependencies and project references, and bundle overrides that omit a published companion entry.
### Scoped-event semantic map
The generated scoped-event subject resolver lives in `dsh-scope`, beside the contract and invariant that consume it. `gen-scoped-events` uses the root TypeScript Program to enumerate `this: Scoped<Base>` declarations, infer routing-key types from real `scopeTarget(base, key)` calls, and require one unambiguous payload subject or an explicit unsupported marker. The committed runtime map imports no event-owner package, so semantic completeness does not expand either the service or scope package's runtime closure.
### Example composition and SDK output
The `dsh-sdk-minimal` patch mounts the service and all four stateful companion subpaths as explicit rows. A subpath entry adds its installable root npm package rather than treating the subpath as a package name. The shipped base-backed config trees omit the service and companions under the [shipped-config decision](../simplification/2026-08-03-omit-invariants-from-shipped-config.md).
Workspace constraints recognize the separate invariant bundle, and package exports, project references, build configuration, dependency declarations, and the lockfile describe the same publication metadata. Generated config catalogs, module graphs, and API documentation derive from those sources.
## Testing
Service tests cover defaults, global disablement, allow/block selection, blocklist precedence, anchoring, unanchored matching, case sensitivity, invalid configuration, zero-match patterns, late registration, duplicate ownership, disposal, rollback, and HMR re-registration. Owners with executable checks keep positive and negative behavior beside the companion source.
Composition tests cover standard-spine forwarding and generated SDK entries. Loader tests preserve each companion namespace, while built plain-Node smokes exercise the compiled subpath exports. The scoped-event freshness gate reruns its semantic Program analysis.
Every Vitest configuration loads a test host that mounts an explicitly enabled service before an ordinary Cordis root's first plugin and adds the current test package's companion when one exists. One exhaustive topology mounts all published companions once; focused service and owner tests construct their own invariant topology so they can exercise disablement, filtering, rollback, and reload without duplicate ownership. Gate tests also execute every published companion's `apply` function and verify that it calls `register` with its manifest name, rather than accepting source text alone.
## Alternatives considered
- **Keep all checks in `dsh-invariants`.** Rejected because the registry would continue importing every checked product domain, owner changes would require central edits, and package tests would remain detached from the contracts they protect.
- **Let root package entrypoints register checks implicitly when `ctx.invariants` happens to exist.** Rejected because root behavior would depend on composition order and optional service presence, diagnostics could not be selected independently, and package loading would hide a registration effect outside an explicit companion.
- **Discover every `invariant.ts` file automatically at runtime.** Rejected because filesystem/package discovery is not a runtime ownership contract, makes bundled publication ambiguous, and cannot express explicit Cordis load order or dependency installation. Build-time generation, verification, and the test host may enumerate the source tree because they validate repository completeness rather than composing a shipped deployment.
- **Validate allow/block entries against the currently loaded package set.** Rejected because a zero-match pattern can intentionally target a later or HMR-loaded contribution; current load order must not determine config validity.
## Consequences
- Product packages own and test their relational assertions while the service stays product-independent.
- Only owners with a meaningful runtime relationship pay the publication, dependency, listener, or trace-state cost of a companion; other packages record the omission reason in their README.
- Compositions that mount the diagnostics can disable all checks or select package names without changing their plugin tree.
- Explicit companion entries make diagnostic cost and ownership visible in Cordis config and package exports.
- One selected contribution adds one child fiber and its listener/state cost, while filtered registrations retain only name ownership.
- Regex sources are deployment configuration and remain fixed until the service reloads.
- Ordinary Vitest roots install the owning test package's selected companion when published; one exhaustive topology pays the full child-fiber cost once for repository-wide registration coverage.
- Session storage validation, snapshotting, freezing, cited source-event validation, and surface acceptance remain always on and are not affected by invariant selection.
A trigger/menu/pick pipeline with zero knowledge of "commands":
- The service holds only the source registry (`InputTriggerSource{trigger: '/'|'@', name, order?, candidates, onPick, matchSpace?, matchEnter?}`; (trigger,name) unique; the optional `order` sorts the roster — lower first, default 0, ties keep registration order — and that sorted roster is both group order and polling order) and `sessionOf(sctx)`. Implementing a match hook IS the declaration of participation in space/enter adjudication; the pipeline polls in roster order, the first non-undefined answer wins, and no claimant means the default sink. matchSpace is synchronous (space fires mid-keystroke; hot cache only); matchEnter is asynchronous (it may await the source's own warmup, and a warmup failure rejects).
- The controller holds the single authoritative hit (span included; retained for Space after the menu closes), the per-session menu store, the candidate-fetch generation, keyboard arbitration (combobox mode: focus stays in the composer surface, ↑↓/Enter/Escape are intercepted and all pass the IME composition guard, with the single exception Shift+Enter unconditionally going first), and pick orchestration (outcome → self-dispatched bail events). `toggleSource(name, syntheticHit)` is the chrome-launch path: it seeds only that registered source over the caller's composer selection and publishes `launcher = name` until close; ordinary typed tracking clears the launcher and restores the full trigger roster. Both paths render the same MenuView and execute the same `onPick` chain. A `dismiss()` verb backs MenuView's injected `onDismiss` (a pointer down outside both the menu and the surrounding composer card closes the menu; MenuView also localizes group titles through the `slash.menu` locale namespace and clamps its height to the viewport space above the composer via ui-primitives' `useAnchoredMaxHeight`); at each session scope's birth it runs `warm(projection)` once over the source roster — within that scope the projection holds only the stable sessionId, with no published/capability transitions; the scope disposer tears down the controller.
- The controller holds the single authoritative hit (span included; retained for Space after the menu closes), the per-session menu store, the candidate-fetch generation, keyboard arbitration (combobox mode: focus stays in the composer surface; ↑↓/Enter/Escape are intercepted; Tab settles a highlighted completion, using the candidate's drill action when available and its ordinary pick otherwise, while no highlight preserves native focus traversal; all arbitration passes the IME composition guard, with the single exception Shift+Enter unconditionally going first), and pick orchestration (outcome → self-dispatched bail events). `toggleSource(name, syntheticHit)` is the chrome-launch path: it seeds only that registered source over the caller's composer selection and publishes `launcher = name` until close; ordinary typed tracking clears the launcher and restores the full trigger roster. Both paths render the same MenuView and execute the same `onPick` chain. A `dismiss()` verb backs MenuView's injected `onDismiss` (a pointer down outside both the menu and the surrounding composer card closes the menu; MenuView also localizes group titles through the `slash.menu` locale namespace and clamps its height to the viewport space above the composer via ui-primitives' `useAnchoredMaxHeight`); at each session scope's birth it runs `warm(projection)` once over the source roster — within that scope the projection holds only the stable sessionId, with no published/capability transitions; the scope disposer tears down the controller.
- Trigger-detection word boundaries (`user@host` and URL `/` never trigger) and the guard tiers (plain: `/` everywhere + `@` inline / claimed: `/` suppressed, `@` live / frozen: none) are the frozen pure core.
### hub / facade: the resident shell and the strict-session input body
- The hub (trigger/decoration registries + send orchestration) takes the slash/command services as optional `ctx.get()` dependencies: without ui-input-trigger or the command surfaces, input still sends and receives normally — graceful degradation.
- Each materialized Session has exactly one `SessionInputShell` (the facade), created and torn down with the session scope; with no session, no input machine is built. `ConversationRoot` is itself the `session-maybe` resident shell, holding HeroShell, the Workspace picker, the composer stack, and the chain-fallback frame. It always owns the same scrollport and composer seat; separate strict-session header and body outlets fill those fixed regions after a Session appears.
- The composer bar is one `session-maybe` slot entry rendered unconditionally: with no session the same InputBar renders inert (machine faces absent, `disabled` owner prop), and once `connectWorkspace` returns a blank session the same instance goes live — the composer surface DOM survives the no-session → blank transition and every later phase flip; `ConversationRoot`, the Hero, and the layout skeleton hold throughout.
- The composer bar is one `session-maybe` slot entry rendered unconditionally: with no session the same InputBar renders inert (machine faces absent, `disabled` owner prop), and once `connectWorkspace` returns a blank session the same instance goes live — the composer surface DOM survives the no-session → blank transition and every later phase flip; `ConversationRoot`, the Hero, and the layout skeleton hold throughout. The memoized InputBar renders its overlay, left, right, and dock child slots after the renderer has bound their standard props; `ConversationRoot` passes only scalar data and callbacks, so an unrelated shell render does not create fresh ReactNode owner props or invalidate the bar.
- ConversationRoot's Hero criterion is `sessionId === undefined || (composerPhase === 'blank' && (openState === 'open' || summaryBlank === true))`: a summary-proven blank Session remains Hero in every open state, while an unproven Session settles during loading. The first submit enters engaging synchronously, and a failure keeps the composer and the error context rather than falling back to the blank Hero; the sidebar's blank bit flips false only after a prompt is successfully accepted.
- Sending unifies in the hub defaultSink: after an optimistic draft clear it goes only through `session.prompt` with `mode:'queue'` (the Web UI has no steer entry; host-wire `mode:'steer'` remains outside this machine); backfill happens only when it fails and the live draft is still empty — a user who has kept typing is never overwritten. No Draft materialize or attach transaction exists.
- When the blank Hero re-picks the Workspace, the shell calls `connectWorkspace`; if the target session differs, the non-empty draft moves from the current shell to the target shell before the new id is opened, and the old blank session survives but is no longer current.
- PickOutcome gains a `{text}` arm; the new scoped bail event `slash/input-insert-text``{text, span}` (the same contract as the other three: draftRev CAS, returning true ⟺ an actual rewrite); facade.insertText goes through setDraft concatenation — zero machine changes.
- Sources get an optional `lexicon?(session)` hook: a synchronous hot-snapshot name roster, with `undefined` = data not warm — zero decoration, never triggering a fetch (the render path stays synchronous and side-effect-free); the paired optional `subscribeLexicon?(session, listener)` hook is the invalidation channel for rolls that change after warm (catalog settles, children spawn/exit). The controller aggregates the rolls into its `lexicon` snapshot store (re-polling on each source notification); sources registered after scope birth are warmed and folded in via the service's live-controller broadcast.
- `decorations.scanTextRefs`: a word-boundary scan of the draft (`/name`, `@name` at line start / after whitespace; `x/name` never hits) against the roster; a hit becomes a `TextRefNode` entity in the Lexical tree (the claim decoration has precedence on the leading-token seat — [the Lexical composer note](2026-08-20-web-composer-lexical-editor.md)); an edit breaking the match shape reverts the entity to plain text.
- Sending is the literal text (no more `<skill>` serialization); on the bubble side MessageItem decorates both shapes (the legacy `<skill>` tag + plain-text tokens).
- `decorations.scanTextRefs`: a word-boundary scan of the draft (`/name`, `@name` at line start / after whitespace; `x/name` never hits; a `/name` token also ends at whitespace or the draft end — the whitespace-bounded shape of the host skill gesture, so `/nfs-hg/xxx` is a path and `/plan。` is prose; the sent-text projection `projectUserText` in ui-primitives applies the same shape) against the roster; a hit becomes a `TextRefNode` entity in the Lexical tree (the claim decoration has precedence on the leading-token seat — [the Lexical composer note](2026-08-20-web-composer-lexical-editor.md)); an edit breaking the match shape reverts the entity to plain text.
- Sending is the literal text (no more `<skill>` serialization); on the bubble side `projectUserText` decorates a plain-text `/name` token only when the same step logged a `skill-invocation` injection for that name — ui-chat's `SkillNameProjector` attaches the step's injected names to the direct message Node, the way the recall projector attaches session labels — so `/123` or a stray `/word` stays plain; a command-input bubble (ui-goal) names its executed command the same way and renders the token as a `command` chip; `@name` tokens still decorate by shape.
- Decoration reactivity: the shell subscribes to the controller's lexicon store and re-scans the document on each roll change, so a roll that settles after the scope-birth prewarm lights existing draft tokens up without any menu interaction or unrelated re-render.
### Per-session provide contributions and the private keyboard surface
@@ -105,6 +106,7 @@ The state machine's entire behavior is covered by pure-JS unit tests (event sequ
| Dual draft persistence {text, occurrences} | The mirror writing the clipboard projection adds zero new concepts; chip degradation across refresh is acceptable |
| The native textarea undo stack | Unreliable under controlled + programmatic writes; the paste two-step undo semantics can only be self-managed — both sides retired with the textarea itself; Lexical's history owns undo now |
| The InputBar receiving a 16-member wiring-callback bundle | The consumption matrix proved 11 members InputBar-exclusive and 1 a dead member; the standard-kit channel lets components fetch their own, with the keyboard surface passed privately in-package |
| `ConversationRoot` rendering InputBar's child slots into owner props | Fresh React elements defeat the bar's memo boundary; the bar already receives `renderSlot` and owns the exact positions |
| Space adjudication also claiming execute-kind commands | The misfire defense: after a space the whole line is an ordinary prompt; irreversible side effects keep explicit entry points only |
| A generic tokenPattern decoration mechanism | Structured occurrence records replace pattern scanning |
| A placeholder select resident in the tool row | Named seats stay empty until registration; a placeholder clashing with the real implementation is two sources of truth |
# Agent Note: The job registry is a capability seam (`dsh-jobs` / `dsh-jobs-local`)
Status: implemented
Archived: 2026-09-04
English | [中文](2026-07-26-job-registry-seam.zh.md)
@@ -16,7 +17,7 @@ The [background-job runtime](2026-06-20-generic-long-running-tool-runtime.md) sh
- **`@deepseek-ai/dsh-jobs-local` (Service Provider)** — `LocalJobRegistry`, the process-local registry: the in-memory store, per-kind id counters, waiter bookkeeping, `TASK_WAIT_TIMEOUT` deadline code, owner-cleanup effects, force-fail teardown, and the default-10 configurable admission policy. Admission derives `running` plus `stopping` capacity from the same records per exact owner, with one unowned bucket; it adds no public count or second state owner. The `dsh-timeout` dependency and Schemastery-owned provider config live here; the Service Definition package has no provider dependencies.
- **`@deepseek-ai/dsh-tool-jobs` (Consumer)** — unchanged; it injects `'jobs'` and never imports provider types.
Compositions load `dsh-jobs-local` where they previously loaded `dsh-jobs` (the CLI cordis.yml row, `agent-spine-demo`, test harnesses, the tool-catalog generator boot). Producer misconfiguration diagnostics ("background jobs unavailable: load …") name `dsh-jobs` — the Service Definition package that declares the absent `ctx.jobs` service — and the Service Definition package's own APIs (its README and the direct-mount fence) point at Service Providers, so the producer message stays correct when another backend becomes the recommended default. Producers, `JobKindMap` declaration merges, and the controller keep importing `@deepseek-ai/dsh-jobs` only.
Compositions load `dsh-jobs-local` where they previously loaded `dsh-jobs` (`dsh-base`, `sdk-minimal`, test harnesses, and the tool-catalog generator boot). Producer misconfiguration diagnostics ("background jobs unavailable: load …") name `dsh-jobs` — the Service Definition package that declares the absent `ctx.jobs` service — and the Service Definition package's own APIs (its README and the direct-mount fence) point at Service Providers, so the producer message stays correct when another backend becomes the recommended default. Producers, `JobKindMap` declaration merges, and the controller keep importing `@deepseek-ai/dsh-jobs` only.
The seam keeps the in-process contract semantics unchanged: `JobStart.run()` still passes callbacks and exact `Agent` objects, so a durable or cross-process backend still has design work to do before it can satisfy this Service Definition (identity, restart, ownership, observation). The split moves that future work out of every Consumer's dependency graph; it does not pre-design the backend.
# Agent Note: Make packed chunk rows the default JSONL layout
Status: implemented
Archived: 2026-09-01
English | [中文](2026-07-26-packed-chunk-rows-by-default.zh.md)
@@ -18,7 +19,7 @@ Reading is unconditional and layout-blind. Packed, unpacked, and mixed files loa
### Logical events and physical rows
The JSONL packing path stays at the `dsh-session` storage seam through `packChunkRuns()` and `decodeStorageRecord()`. The encoder recognizes exact delta-event shapes, preserves unrecognized events verbatim, and packs only runs of at least three. A packed row is encoding vocabulary, not a `SessionEventMap` member: it never enters `Session.events` or fires `session/event`. The [packed session-history transport decision](2026-08-15-packed-session-history-transport.md) reuses this vocabulary for a bounded lossless wire interval without changing those event semantics.
The JSONL packing path stays at the `dsh-session` storage seam through `packChunkRuns()` and `decodeStorageRecord()`. The encoder recognizes exact delta-event shapes, preserves unrecognized events verbatim, and packs only runs of at least three. A packed row is encoding vocabulary, not a `SessionEventMap` member: it never enters the Session log or fires `session/event`. The [packed session-history transport decision](2026-08-15-packed-session-history-transport.md) reuses this vocabulary for a bounded lossless wire interval without changing those event semantics.
The JSONL backend packs each durable append batch. Raw `compression: 'none'` and default Zstandard framing carry the same logical storage records; selecting raw mode for reviewable fixtures does not disable packing. Repository replay readers and normalizers decode the shared row format instead of maintaining snapshot-specific codecs.
- **`session/`** is the durable session data plane: the persistence seam with its backends and checkpoint policy, the projection fold that serves whole values from that log, log-backed titles, and OTel reporting. The title fold is itself load-bearing for the read side (`session-query` peer-depends on `dsh-session-title`), so titles belong with the data plane, not in a derived-services annex. The plain name is deliberate (prefer names a human would say); the nearby `core/session` package remains the live in-memory service, while this group is the durable family around it. `session-query/` stays a standalone group — the read/tool surface has its own model tools and SQLite FTS backend and is consumed independently of persistence internals.
- **`session/`** is the durable session data plane: the persistence seam with its JSONL provider and checkpoint policy, the projection fold that serves whole values from that log, log-backed titles, and OTel reporting. The title fold is itself load-bearing for the read side (`session-query` peer-depends on `dsh-session-title`), so titles belong with the data plane, not in a derived-services annex. The plain name is deliberate (prefer names a human would say); the nearby `core/session` package remains the live in-memory service, while this group is the durable family around it. `session-query/` stays a standalone group — the read/tool surface has its own model tools and SQLite FTS backend and is consumed independently of persistence internals.
- **`interaction/`** is the human-collaboration plane plus the terminal channel that answers it: the question/approval seams, the permission preset, the model-facing `ask_user_question` tool, the human-command registry (`plan-mode` and `command-goal` already consume `commands` together with the interaction seams), and `tui` — the interactive channel is the plane's richest provider and consumer (peer edges to `commands` and `user-questions`), and a one-package `tui/` group would spend a top-level name on one plugin.
- **`boot/`** is a role-complete single-package group: the shared boot glue that belongs to no channel and no assembly (consumed by `apps/cli` and test-only Loader drivers).
- **`guard/`** keeps its documented role, loop-hygiene guards, and gains the tool-call timeout enforcer, dissolving the one-package `timeout/` group whose name collided with `util/timeout`.
Loaded 100 of 12185 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.