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.
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.
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.
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.
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.
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.
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.
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.
The loader's entry store is a plain object, so an auto-generated all-digit
id is reordered ahead of its siblings by integer-key semantics and the
previous ordered assertion flipped on roughly one in fifteen runs. Row
ordering belongs to loader.entries(), not to this projection; the spec now
asserts each row by its id.
CI's coverage lane runs on a clean tree where workspace imports resolve
through tsconfig paths to src; the value import of
@deepseek-ai/dsh-agent-presets/display therefore needs its own paths row
beside ./types. Locally the built lib masked the gap; reproduced by moving
lib aside, fixed, and re-run green under the same condition.
Review round on #3316: the composition inventory answers from a standing
mount before the broken verdict (a file corrupted after mounting no longer
hides the running composition), livePresetMounts filters by the caller's
root fiber so a second Cordis runtime in one process never answers for it,
compositions carry trust and the plugin list resolves shipped preset names
through the shared dsh-agent-presets/display fold over ui-agent-preset's
dictionaries (INLINE_SAFE inline import; no cross-plugin runtime import),
the condition detail label reads Disabled when/禁用条件, and the stale
four-surfaces comment says three.
Reconciled with the scope-grouped plugin list: compositionInventory keeps
its additive block over master's RemoteError refactor; the slimmed
settings-store keeps ctx-based signatures while staying a display-only
roster store (no describeFace, no select); dead transport-rejection tests
dropped with the wire that no longer rejects; module graph, catalogs, and
harnesses regenerated against the merged tree.
Both scope groups drop their boxed chrome for the General-settings row
idiom: a title row (session-plugins title with the right-aligned preset
selector pill; global-plugins title), a grey subtitle line carrying the
count, a hairline divider between groups, and the cards grid below.
Counts ride the subtitle as text while the data attributes keep the raw
numbers for tests.
Review-driven refinements to the scope-grouped plugin list:
- The preset switcher becomes the General-settings selector pill over the
shared Menu primitive instead of a native select, and the preset group
is collapsible like the global one (search still forces both open).
- The session-plugins drawer is removed: rows the presets took over sit
inline in the global list with the preset-provided tag and per-preset
details, since the preset group above already shows those compositions.
- The status dot renders only for a live root fiber, so file-state rows
of an unmounted preset carry their enablement tag alone instead of a
column of grey dots.
- PresetTree reclaims the owning entry's subtree slot: EntryTree's
constructor filed the standing mount under the roster's own Loader row,
so after the first session composed a preset the whole composition
leaked into root loader.entries() as host rows (each preset overwriting
the last). A regression test holds the root entry list identical across
a mount.
The settings plugin list projected ctx.loader.entries() alone, hiding the
plugins sessions actually run and rendering the web overlay's deliberate
disabled tombstones (tool-bash, tool-fs, ...) as two dozen plainly disabled
rows while the same modules ran in every standard-preset session.
- dsh-agent-presets: compositionInventory() answers flattened rows per
preset — newest live standing generation when mounted, composition file
otherwise with !!js disabled gates evaluated against the Loader context;
reading never mounts (regression-tested), refusal stays 'conditional',
raced files report broken with the reason.
- dsh-host-plugin-inventory: list() gains an optional agentPresets block,
resolving the roster as an optional peer and mapping fiber states to the
public phase vocabulary.
- ui-settings-plugin-inventory: preset group first behind a display-only
switcher opening on the default preset; global group collapsed with
failures floated; host-disabled modules enabled by >=1 preset fold into a
session-plugins drawer naming providers; search spans scopes and points
at matches in unselected presets.
- ui-agent-preset: the General-settings default-preset row is deleted — the
roster section's make-default and the new-session chip keep the field —
and the settings store slims to the display roster the header label reads.
Docs, catalogs, module graph, settings-chrome goldens, and the bilingual
Agent Note ride along.
A pointer drill in the @ menu left no breadcrumb and a crumb click dropped
the header, while the keyboard drill worked. InputTriggerController.settle
assigned `drilled` after execute() returned, but a pointer mousedown runs
outside any Lexical update, so the descent edit commits discretely and
re-enters track() during execute() — where refreshHeaders and
fetchCandidates both read the claim while it was still clear.
settle now claims the drill before dispatching the edit and withdraws it
only when the edit is refused, which mutates nothing and so drives no
re-entrant track().
The skill carries the reliability rules, but nothing an agent loads by default
said that specs run concurrently at all. The testing policy described tiers and
evidence without ever stating how a spec is executed, and neither subtree
AGENTS.md mentioned it — including scripts/, where the two suites that recently
failed on unrelated branches live.
docs/testing.md gains the execution model as the one home for the fact: forked
workers, concurrent coverage partitions beside other gates, and self-hosted
runners sharing a host and volume, with the rule that a spec passing only when
run alone is a defect in the spec. It links the skill for the detailed rules.
packages/AGENTS.md and scripts/AGENTS.md carry the short actionable form and
link that section, so the rule is present in the context loaded while a test in
either subtree is being written.
Both ceilings are raised for the added words and the targets in docs/AGENTS.md
move with them: docs/testing.md 1150 to 1300 (now 1237) and packages/AGENTS.md
675 to 750 (now 712), each keeping the 5% headroom the standard requires.
The 'Test strength' bullet carried assertion strength, external-state
verification, the reliability reference, and the coverage caveat at once.
Splitting the reference into an adjacent entry matches how the orientation
list above names the same skill, and restores 'Test strength' to one subject.
The new entry also names the platform and timeout-budget rules the skill now
carries, so the checklist covers what a reviewer is being pointed at.
Two failure classes the repository paid for are not covered by the isolation,
synchronization, and teardown rules already in the skill.
A value the operating system owns is not guaranteed to return as written. A
test may write one back only where the assertion tolerates that write-back
failing; where the assertion depends on it, the expected value comes from a
fresh read. NTFS truncating a fractional-millisecond mtime and Windows folding
environment variable name case are the two instances seen so far.
A describe or case timeout overrides the runner's --testTimeout rather than
yielding to it, so a value below the lane budget lowers what CI granted, while
the same literal reads as a widening where the host default is smaller. The
hook budget travels with the test budget, and a case asserting a timeout keeps
its outer wait far larger than the timeout under test. Restoring a granted
budget, or sizing a bounded retry to measured contention, is named as distinct
from the masking fixes the skill rejects.
The diagnosis reference gains the platform differences under its platform
class, a classification path for self-hosted pools that expose no host metrics,
and the stopping rule for a signature no available host can reproduce.
The Windows coverage lane grants DSH_COVERAGE_TEST_TIMEOUT_MS=90000, but two
paths declined it.
scripts/install-lefthook.spec.ts took a describe-level 30_000, restated as a
per-case constant on five cases. Every case drives spawned Git and Node
subprocesses; the slowest costs 7.5 s on an idle host, so the ceiling carried
roughly fourfold headroom and fired on branches that did not touch the file.
The suite takes 90_000 and the redundant constant is removed.
coverageTestTimeoutArgs raised --testTimeout and --expect.poll.timeout but left
--hookTimeout at Vitest's separate 10 s default, which removeFixtureSafely's
documented 10-second Windows retry window meets exactly. Raising only the test
budget would move a contended suite's failure into its teardown.
- api-gateway reference (en/zh): gateway/lookup-unavailable and
gateway/internal replace the pre-convergence codes, and the resolver
paragraph states the RemoteError pass-through semantics.
- failure-vocabulary note (en/zh): the package is dsh-util-time, the
marker is isDSHRemoteError, and discrimination requires no
instanceof at all.
- protocol: isDSHRemoteGatewayError -> isDSHRemoteError (the class is
protocol-wide, not Gateway-specific); remoteErrorOf drops its
instanceof Error precondition and tests the marker plus a string code
structurally, so a marked failure from another realm no longer reads
as a local defect.
- session-controller: the live-follow packed-record protocol violation
now throws a marked RemoteError('gateway/internal'), landing the
session in openState=error instead of an unhandled rejection;
pinned at the transport and session levels.
- util-time: correct the invariant companion's @module name.
- regenerate the tool-cordis catalog for the marker rename.
- api/gateway: $host caches its RemoteHostFacts record and mints a new
one only when home changes, so snapshot readers compare by reference;
identity pinned in the client spec.
- client/ui-tool, client/ui-workspace: the hooks channel exposes the
host facts record as hostInfo and components select the field they
need (useHostInfo(info => info.home)); row-component contracts are
unchanged.
- gen-cordis-catalog / gen-cordis-inspect-catalog / gen-client-catalog /
gen-config-catalog / gen-doc-graphs / gen-module-graph outputs pick up
the converged Remote vocabulary (gateway/* codes, RemoteError JSDoc)
and the dsh-util-time package; the zh sides of the three
English-generated pages follow the same line-number shifts.
- the message-feedback protocol golden records the accepted wire
change: a boundary-validation failure now reports
gateway/input-invalid with structured details instead of a bare
internal code.
- new cookbook page adding-a-remote-api (en/zh): the five-step HOW-TO
for declaring, failing, registering, consuming, and testing a Remote
endpoint.
- new Agent Note ctx-remote-failure-vocabulary records this round's
decisions and alternatives; the 2026-08-02 and 2026-08-10 notes are
rewritten to the shipped facts (RemoteError vocabulary, $host, the
retired ApiProxy statements).
- package READMEs pick up the new failure-face contracts
(typert/protocol, api/gateway, api/remotes,
test-support/client-runtime), dsh-util-time gains its README and
registry entries, and stale connection/WorkspaceError/legacy-code
statements are corrected (ui-settings, ui-settings-models,
workspace-controller, docs/subsystems/typert incl. the
TypertGatewayErrorCode type-equiv block).
- packages/AGENTS.md gains the Remote-failure rule bullet; its doc
budget rises 675 -> 714: the bullet is the compressed remainder
after relocating detail to the cookbook and the Agent Note.
- api/gateway: mark terminal Remote-stream escapes (carrier retry
exhaustion and pre-acceptance end classification) as
RemoteError('gateway/internal') at the two escape points; marked
failures pass through verbatim. The carrier class stays the
retry-internal signal for carrierFailed and the ended(true) retry
trigger. Regression coverage lands on the session and workspace
stream consumers.
- client/ui-tool, client/ui-workspace: read $host.home through a hooks
observable subscribed to connection/reset; the slot renderer memoizes
inject results per entry, so the previous plain-value injection froze
home at the first render.
- client/ui-settings-models: components no longer receive ctx; apply
binds the credential and settings Remote operations into callbacks,
and the settings/conflict code judgment stays in the apply world.
- classify a carrier throw under a caller-aborted signal as
gateway/cancelled instead of gateway/internal, matching the code the
Host produces when the abort wins the wire round-trip.
- read $host facts from the construction-time Connection handle,
matching $stream; the service cannot be replaced without restarting
this plugin, so the live re-lookup was dead complexity.
- state rebuiltFailure's actual contract in its comment: codes pass
through verbatim without runtime validation.
- correct the @module name in dsh-util-time.
- python/sdk-runtime: add @deepseek-ai/dsh-util-time so the runtime
dependency closure stays closed (dsh-subagent now depends on it).
- api/gateway: drop the dsh.client.external request for
dsh-typert-protocol and admit the protocol package into INLINE_SAFE
instead. The loader module table has no supplier for the protocol
package, so the built client factory threw at require time; the
protocol layer is duplication-safe by design (string marker,
code-based discrimination), which is the inline-safe admission
criterion.
- api/gateway tests: cover the $host getter (live service read,
pre-ready home, and the construction-time fallback after the
Connection service is withdrawn).
Single RemoteError with a merge-extensible, domain-prefixed code map;
owners throw at the failure point; streams surface marked failures;
clients consume ctx.remote directly with isRemoteFailure as the only
discrimination point and construct no failure instances.
Move hover enter/leave from the svg element to the stationary fishHitbox
span so the CSS sway and SMIL morph share the same trigger surface and the
animation cannot flicker when sway displaces the svg from the pointer.
Gate the morph with (hover: hover) to match the CSS and prevent tap-sticky
loops on touch devices.
Remove residual headline changes unrelated to this PR: white-space: pre on
.headlineText, data-testid="hero-headline", and corresponding getByTestId
test assertions — these belong to #2397.
Fix Agent Note coverage claim (was "asserting render states", now accurately
"slot contract") and compress blowhole paragraph to current-state fact.
Retained rows made arbitrate('enter') claim 'pick-highlighted' while
pick() silently declined the pending group, so the key vanished by
coincidence. Check the highlighted group's readiness like Tab does and
return 'consumed' deliberately; record the stale-while-revalidate menu
decision in an Agent Note.
`JsonlSessionPersistence.exists` treats ENOENT as absence and stats the
path's parent first, so a session directory blocked by a regular file
stays a storage fault. Only Windows needs that: it reports ENOENT rather
than ENOTDIR for `regular-file/child`, while POSIX open reports ENOTDIR
before the branch is reached.
The stat ran on every platform and every absent probe. findLog issues
four probes per project directory and the coordinator resolves an id
twice per inspect, so nearly every probe paid it. Counting fs calls
against a five-project store, loading an existing session drops from
40 open + 41 stat to 40 open + 3 stat.
The Turn fold publishes the first step's TTFT (contract/turn-metrics.ts
firstStepTtftMs), never an average; only the Session StatsLine averages
across steps. Rename the dialog row in both locales and align the Agent
Note with what actually ships.
The 2026-08-04 bug-fix note owned the overflow-x clip that existed only
for the glow's bleed; with the glow, the clip, and its test all gone the
note is fully superseded. The new note preserves its rationale and the
reintroduction condition for future bleeding chrome.
conversation-column-overflow existed solely to verify the glow bleed was
clipped; delete it with its golden. The geometry golden picks up the
scroll body losing overflow-x: hidden. reference-composer now waits for
the stale '@' rows to settle before clicking: the menu keeps the
previous query's rows while the next loads, and index-keyed rows swap
content in place.
The turn-stat dialogs copied ContextMeter's document-listener effect, which
the duplication gate flags; useDismissOnOutsidePointer gains an optional
portal ref so the portaled dialog counts as inside, and TurnUsagePanel keeps
only the Escape listener. The icon-count test also learns the two pill
glyphs the branch added.
The blurred blue ellipse (HeroGlow) below the homepage input card reads as
stray tint rather than intentional chrome; drop the component and its
positioning/overflow scaffolding.
Batch of visually verified Web UI fixes: trigger-menu z-index over the
resize handle (#3228, #3229), input scrollbar offset, tool-row file
links and diff stats (#3230), @ menu crumb alignment, light-mode
divider, and @ menu flicker while typing (#3234) via
stale-while-revalidate — a refinement hit keeps the previous items and
highlight on screen until the new generation settles, so neither the
skeleton nor the first-row focus blinks per keystroke.
Every case in the suite drives real git invocations against a scratch
repository, so it is bound by process creation rather than by its assertions.
The describe-level 15 s capped all 23 cases below the 90 s the Windows coverage
lane passes as --testTimeout, and the suite has been observed timing out at
15000ms on a branch that did not touch the file.
Refs #2677.
The A/B test settled on the twin-pill layout, so the TEMPORARY
?usage-variant=flat trigger, its locale keys, and its tests leave with it.
Below 480px the stat pills now take the sibling action-button geometry so
their bare icons keep the row's rhythm instead of drifting on the wider
label padding and the -6px pair rebate.
The turn tail's exposed meta line collapses into two icon pills — Usage
(database glyph, turn total) and Ran-for (clock glyph, wall time) — each
click-opening a details dialog; the calendar clock trails as plain text.
Cache hit, TPS, and TTFT move dialog-only, and narrow viewports collapse
the pills to bare icons. The TEMPORARY flat variant keeps the whole-line
trigger for the A/B test.
Review bot findings: DSH_TOOLS_MODE values, the wire-replacement
wording, 'code-only' mode references, and the spill README's
dispatch-log waterfall name all still named the removed 'code' value.
The review bot found stale 'code' configuration values in the zh tools
and agent-tool-presentation READMEs and the zh tool catalog, which the
runtime schema already rejects in favor of 'ptc'.
Master removed the ApiProxy package; its former test paths now live in
the client connection, API gateway, and settings controller test
directories. Update the three notes' references so verify-package-paths
resolves.
The rebase merged master's SDK-example and telemetry prose with the
ptc renames in the tools and CLI reference READMEs and the executor
collapse note; re-record their pair hashes.
The DSH_SNAPSHOT refresh recorded the internal seq-range form; the
canonical fixture layout expands those ranges. Apply the mechanical
migration instead.
Master replaced the session-log-version-mechanism note with the
fail-closed-session-event-vocabulary note; update the rename note's links
(en/zh) so cross-links resolve.
The subagent review found stale code mode-value prose the mechanical pass
missed: tools and agent-tool-presentation READMEs (en/zh), the CLI reference
page (DSH_TOOLS_MODE and the preset roster), the zh tool catalog, the
execute JSDoc and collapse comments in dsh-tools, the codeModeHarness
helper in agent-loop tests, and stale code-mode titles in ptc.spec.
Apply the rename pass to READMEs and docs the master sweep rewrote, fix
PTC mode anchors and the renamed-note links in the spill READMEs, and
regenerate the doc graphs.
The split kept the session-persistent vocabulary (tool/code-dispatch*,
tools-code-mode, :code:) on this PR, but several prose surfaces still named
the new values: the zh persistence/tool catalogs, the renamed Agent Notes'
event mentions, spill-policy comments, and a garbled 're-enPTC mode'
replacement. Also rename the mode value to ptc in the places the rename
missed (tools and agent-tool-presentation READMEs, the Config JSDoc, note
mode unions) and the codeModeHarness* e2e helpers.
Rename the tool-presentation transport from code-mode to ptc everywhere
that is not written into session logs: the mode config value becomes 'ptc',
the preset directory/id becomes ptc, the demo becomes demo:ptc, the
dispatch waterfall becomes tools/ptc-dispatch-log (types PtcDispatch*), the
prompt rule becomes tools:ptc-only, source/test files become ptc.ts etc.,
and prose says PTC mode / PTC 模式. The session-persistent vocabulary
(durable events tool/code-dispatch*, logged plugin name tools-code-mode,
sub-call id segment :code:) intentionally stays and moves in the stacked
persistence PR, which is blocked until the SESSION_FORMAT_VERSION v0→v1
migration lands with it. run_code, its code parameter, CodeSdkLanguage,
CodeRunFailedError, the dsh-code-runtime family, third-party codex names,
and frozen archived notes keep their names.
The whole-line meta trigger read as plain text and hid what was clickable.
The Turn-usage trigger is now a data-icon pill (Usage {total} · Cache hit
{percent}%) seated right of the branch action with the action buttons'
hover chrome, so the one interactive element in the tail is visibly a
button; the timing facts (clock, run time, speed, TTFT) return to plain
non-clickable text behind a dot separator. The details dialog keeps the
Turn-usage title and full token buckets, gains a permanent cache-hit row,
and breathes with wider vertical padding. Narrow columns trim the pill
label to an ellipsis instead of widening the chat column. User rows and
turn tails share the recency gate: only the latest row of each kind keeps
its actions visible without hover.
chmod 0 can only deny directory reads on POSIX to a non-root owner:
Windows exposes no directory permission bits for readdir, and root
bypasses them. Where the fixture stays readable the sealed candidate is
indexed, so the unreadable-branch behavior is pinned on POSIX non-root.
An injected readdir failure keeps that branch covered on every platform.
Replace Vitest's hash-based --shard with a coordinator-side
longest-processing-time assignment. The coordinator collects the
instrumented inventory from a vitest list run (dropping the exempt
heavy suites that list does not exclude), reads per-file durations from
the Vitest results cache, and seeds heavy subprocess-bound suites into
different partitions. A weight-aware test fails when assignment ignores
recorded weights, verified by injecting a file-count-only rule.
Windows coverage measured partition spread of 442s (275-717s) under
hash sharding; a simulation with the same file durations and the new
assignment balances partitions to within 21s, cutting the critical
partition to roughly half.
collectPackageNames repeated collectPackageAliases' directory walk
verbatim, which jscpd reported as a 7-line clone and which failed the
lint-and-duplication gate on both the Linux and Windows lanes. Both now
read one workspacePackages() iterator: the alias collector keeps only
packages named after their directory, and the coverage collector keeps
every one of them.
Review found the CLI entry guard compared import.meta.url against a
concatenated file:// URL. That fails whenever import.meta.url encodes
something process.argv[1] does not — a repository path containing a
space, or any Windows drive path — and it fails silently: the script
exits 0 having done nothing, so verify-tsconfig-paths would pass
without checking exactly where it is needed. Running a copy from a
directory whose name contains a space reproduces it. The guard now uses
the comparison the repository's seven other generators already use.
Deleting the group wildcards also removed the fallback that resolved a
package nobody had aliased, so the generator now asserts coverage:
every workspace package with a src directory must be mapped, and
--check names any that is not. That assertion immediately found four
packages named dsh-<group>-<directory> whose siblings carry hand-written
aliases while they did not, so they reached built lib/ output through
the workspace symlink. They now carry aliases too, which takes the
resolution differences in this branch from seven to eleven.
Two comments in tsconfig.base.json still pointed at the deleted
wildcards; they now state why those aliases stay hand-written. The
package-inventory proposal recorded the wildcard collapse as current,
so both notes are cross-linked as partial supersession.
The rebase onto master's turn-process folding kept master's makeHarness
rework, which dropped the turnUsages passthrough and the single-trigger
footer assertions; re-merge both sides.
Replace the two-row footer (TurnUsageDisclosure + icon-row clock chrome)
with a single whole-line trigger (clock · run time · turn usage · cache
hit · speed · TTFT) that opens a per-Turn usage dialog. The latest turn
keeps its tail always visible; older turns reveal the whole row on
hover/focus. Turns without usage data keep the plain clock line with
identical spacing.
* feat(ui-conversation): fold turn process before final answer
* fix(ui-chat): polish turn-process control row from review
* test(web): drive preset slash catalog with gestures
* fix(ui-chat): keep turn process order stable
* fix(ui-chat): preserve prompt order after pagination
Co-authored-by: Yif <[email protected]>
tsconfig.base.json resolved @deepseek-ai/dsh-* through 49 candidate
globs and @deepseek-ai/dsh-*/invariant through 45, one per package
group. Resolution tries candidates in order, so a package late in the
list paid for every earlier miss — and under the dsh source launch each
miss is an ERR_MODULE_NOT_FOUND that Node decorates with a full
CommonJS resolution walk. A boot profile attributed 934.6 ms, 35% of
startup, to that decoration path across 60,942 failed resolutions. The
cost landed hardest on packages/util/*, which sits at position 44 of 49
and holds the leaf utilities nearly every plugin imports.
gen-tsconfig-paths writes one explicit alias per package into a marked
region and both group wildcards are gone; verify-tsconfig-paths reports
drift and runs in the ci-static lane. Booting the headless profile from
source drops from ~2,157 ms to ~1,055 ms with --help output unchanged.
All 1,022 dsh specifiers in repository sources resolve to the same
target as before, except seven /invariant specifiers in the lsp,
terminal, and runtime-diagnostics groups that the deleted wildcard
never listed: those reached built lib/types instead of src, against the
rule that static gates resolve through paths to src on a clean tree.
Progressive enhancement on body in the shell base sheet so mixed
Chinese/English copy gets consistent spacing without content edits;
engines without support ignore the property.
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
A title now comes from an attached session's live projection cut or a cold
one's durable checkpoint, and from nothing else. Attachment is decided by the
session store at read time, so a session that attached after the listing is no
longer answered from a checkpoint its log has moved past — the stale-title case
`api-session.list` already handles this way.
The log fold and its per-log memo are gone. Folding one title costs a whole
log, and this call sits under every keystroke; a session no projection answers
for is labeled by its id and regains its title the first time it is opened.
`lib` leaves the default exclusions: Ruby gems and many npm packages keep
sources there, and the miss would be silent and total. A traversal whose root
is unreadable now rejects instead of publishing an empty index over entries
that are still good, which is what the stale-while-revalidate path claimed but
could not do while every readdir error was swallowed. A drill marks the menu
drilled only when its edit actually reached the draft.
Refs #3154
Refs #3180
Review found the shared observation defeated the very fence it fed:
it carries the original PID-to-start-time pairing forward, so a
recycled PID still matches it and takes a signal meant for the process
that exited. Capturing it outside the per-member try also let one
failed read abort a whole teardown round, breaking the synchronous
host-exit contract, and an empty round paid a read for no members.
signalProcess now reads ProcessInspector.isAlive immediately before
delivering, from the narrowest per-identity source each platform
offers; signalMembers and waitForMembers return before capturing when
a round has no members. snapshot() keeps serving the readiness poll,
whose per-poll table read stays at one.
Windows enumerates Toolhelp32 lazily on the first tree question, so a
snapshot asked only for liveness — the 25 ms teardown poll — performs
no table walk at all.
Replace the debounce-timer polling with Include.stop(), which flushes the
file-backed write queue deterministically. This avoids Windows coverage
flakes where the self-dispose write could land after the expect.poll
timeout.
TurnNavigator was an unmemoized component rendering one div and one
button per loaded Turn, so every ChatView render rebuilt the whole rail:
143 button rebuilds per commit in a 300-Turn session against 8.4 in a
4-Turn one, while a streaming answer commits dozens of times.
memo alone would not have helped, because navigateToTurn was rebuilt on
every render and broke prop identity, so it moves into useCallback.
MacProcessInspector answered the descendant tree and every member's
liveness with its own `/bin/ps` fork, so one readiness poll cost N+1
full table reads for N tracked descendants. With execFileSync on that
path and a 50 ms poll interval, any command spawning two or more
children saturated the host event loop until it exited.
ProcessInspector.snapshot() now returns one ProcessSnapshot that
answers tree, session, and alive from a single observation, and
signalProcess takes the caller's observation so its PID-reuse fence
does not re-read the table per member.
Collapsing every relative-time bucket to `{{age}}` reached the session-tree
goldens, where a literal age is the assertion: a fresh row reads `now` and an
older one does not. Six e2e files failed, two of them by aborting mid-scenario
and leaving their replay fixtures half-consumed.
`captureStableAria` now takes the rule as an opt-in, and only the reference
menu — whose rows are dated from the live Host list — asks for it.
Refs #3154
The Host session list already carries each session's `updatedAt`, which is
the number its own rows show; reading it there keeps the two surfaces from
disagreeing and avoids making a context capability depend on the BFF
assembly that owns the `sessionListMetadata` projection. A session the list
does not carry falls back to the candidate's creation time.
Refs #3154
Session candidates labelled from projection checkpoints instead of a full
log fold per keystroke, with the uncheckpointed remainder folded once and
memoized while its log stays cold. The file index keeps answering while an
invalidated traversal rebuilds behind the caret, and its default exclusions
now cover build outputs so deep sources stay reachable.
Rows carry only what distinguishes them: a file names its parent directory,
a session names its workspace only when that workspace is not the current
one, and a drilled listing names none because its new breadcrumb does.
Resolves#3180
Related to #3154
Stacked on #3115: keep its windows-* setup-pnpm-js-<run_id>-<run_attempt>-<job>
destination, and extend the same isolation to non-Windows jobs in ci.yml
and ci-master.yml with setup-pnpm-<run_id>-<run_attempt>. This prevents
sequential self-hosted Windows jobs from tripping over a stale locked
pnpm.exe/reflink native module.
The anchors-a-relative-add-spec case serializes two subprocesses (plugin
add + remove) under a hardcoded 90s budget, which the 2x60s worst case
exhausts; derive it from SPAWN_TIMEOUT_MS * 2 + 30s like the other
dual-call cases. The pnpm setup isolation note now also records the python
SDK exe build's suffixed destination and its regression-test coverage.
The merge resolution had reverted THIRD_PARTY_NOTICES.md to the SDK 0.3.220
rows; the lockfile pins 0.3.241, so CI regenerated a mismatch. The apply spec
now drives the injected onHover face, closing the per-file coverage gap on
src/client/index.ts.
The plugin add and dump-default-config cases serialize two runBuiltBin
calls, each with a 60s execa cap; the 90s outer budget could be exhausted
before the second call. Raise them to SPAWN_TIMEOUT_MS * 2 + 30s, matching
the multi-call treatment.
Candidate rows lead with domain icons instead of localized text
prefixes; pointer and keyboard share one reducer-owned highlight (last
input wins); drillable folder rows reveal a localized Browse-folder +
Tab keycap hint with the library chevron; pending sources render
skeleton bars; the menu spans the composer card. The editable @dir/
text decorates color-only — the domain icon now belongs exclusively to
the settled reference chip. Composer placeholders advertise / and @,
and the zh copy for commands is unified to 指令.
The Agent Note (both languages), the PR prose, and the commit message
claimed a 13px floor for the table variants; the formula has none —
max(13px, setting − 2px) selects the −1 branch at low settings rather
than clamping the result, so the tier bottoms out at 11px at the 12px
setting, matching think text. Rewrite the claim, say so in the axis
comment, and split the README sentence that lumped body-pair and
secondary-pair consumers together.
Assert the engine-resolved secondary size in the settings-chrome e2e
(13px at the default, 13px at the 15px boundary, 14px at 16px,
unchanged across reload), sync the StatsLine and workflow-panel spec
headers with the tier they now pin, and note why memberLabel stays at
the body size.
Derive --dsh-content-font-size-secondary (setting -1 at <=14, setting -2
above; 13px at the default) with --dsh-content-font-delta-secondary in
gradient-shadow-text.css, and move every one-step-under-the-body text
onto it: think text and reasoning summaries, the shared DisclosureRow
title, ToolRow and bash-row summaries and file links, compaction/
context/command/retry/error rows, StatsLine, the workflow-run panel
tiers, reference summaries, the turn-status clock, and the feedback
note trigger. The markdown table variants join the same tier instead of
staying fixed; its 13px floor keeps them legible at the 12px setting.
At the default setting the flow-row titles and summaries render at 13px
(previously 14px) so they match think text at every setting instead of
sitting 2px above it.
The requires-profile and routes-help cases serialize 4-6 runBuiltBin calls,
each with a 60s execa cap; under the loaded pool the 90s outer budget was
exhausted before the last call and vitest truncated the run without the
execa diagnostics. Raise both to SPAWN_TIMEOUT_MS * 3 + 30s.
* ci(build-exe): drop pull_request label trigger to avoid skipped checks
* docs(build-exe): sync agent note and pin event set in workflow spec
* test(ci): type-safe event key assertion for build-exe workflow
* ci(build-exe): use present-tense trigger comment and drop label-run note
The built-bin help/usage case still used a win32-conditional 60/30s outer
budget while serializing six runBuiltBin calls, and startProfileLifecycle
lacked the execa timeout/killSignal the sibling helper has; the tool-ralph
cases pinned 20-30s explicit timeouts that the 90s lane default cannot
override. Align all of them to the SPAWN_TIMEOUT_MS + 30s (or 90s) pattern.
The per-case 15-30s budgets on the Windows native and coverage lanes fire
before oxlint, workflow-worker-thread, and other subprocess-spawning cases
finish under the loaded self-hosted pool; the failures rotate across cases
as load shifts, so per-case widening only moved the flake. Raise the lane
defaults (DSH_COVERAGE_TEST_TIMEOUT_MS and the native --testTimeout) to 90s,
align the oxlint and workflow-worker-thread case budgets, and keep the
built-bin SPAWN_TIMEOUT_MS at 60s under a 90s outer budget.
The provider-card seat's keyConfigured now derives from the reference the
page would use — the profile's apiKeyEnv, or the page's derived
<ROUTE>_API_KEY while the profile names none — so the add-provider draft
agrees with its own editor about an existing conventional credential (the
store joins the derived describe in the same batched call, as
ProviderRow.derivedCredential). ModelsSectionProps makes the renderSlot seat
required so a direct render that forgets it fails to compile; the one such
render in provider-form.client.spec regained a real mount and the test
boilerplate collapsed to renderSlot={() => null}. The extension-slots Agent
Note now states the keyed cell's real override rule (same priority throws,
a different priority shadows), and docs/subsystems/slots.md carries the two
new seats in its hierarchy, both languages.
The windows-* jobs now install pnpm under a run/attempt/job-suffixed
destination, so the pre-install step that cleared the old fixed
setup-pnpm-js path no longer touches the actual destination and its
comment claims stale state. The suffix already gives every job a fresh
directory, so remove the four cleanup steps.
The execa timeout was widened to 60s but the outer vitest case budgets stayed
at 30s, so a cold-starting built bin would trip the vitest budget first and
the execa SIGKILL cleanup could not run inside it. Extract SPAWN_TIMEOUT_MS,
share it across the execa deadline, its error text, waitForFile, and the
outer case budgets (60s spawn + 30s headroom), so the widening is coherent.
Both suites spawn real subprocesses (oxlint probes; the dsh built bin) that
cold-start slowly on the contended self-hosted Windows pool, so their 20-25s
timeouts fire before the child finishes. Raise the oxlint contract case
timeouts to 60s and the built-bin execa timeouts to 60s, matching the
tool-ralph budget treatment.
The windows-* jobs keep a separate standalone pnpm executable under
runner.temp/setup-pnpm-js. A previous job on the same self-hosted runner
can leave a locked @reflink native module there, so the next job's
pnpm/action-setup fails with EPERM during unlink before any test runs.
Suffix the destination with run_id, run_attempt, and job so every job
gets a fresh directory even when sequential jobs land on the same
runner; apply the same to the python SDK exe build. Update the pnpm
setup isolation note to record the Windows-specific destination.
pi-ai credentials carry optional members as explicit undefined (a github.com
Copilot grant holds enterpriseUrl: undefined), and the store bridge committed
the object verbatim, so the credential store's strict validator refused the
write and sign-in failed after the provider had already authorized it.
toRecord now drops explicitly-undefined members and renders undefined array
entries null, exactly as JSON.stringify would; everything else passes through
untouched so genuinely unstorable values still fail loud at the store.
Review follow-ups: wait on the caller's own locator instead of assuming
it is the page's first composer, describe the actual read-only window
(submit adjudication and locked states — a running turn stays editable
for queueing), drop the pre-Lexical narration from the JSDoc, and record
the gesture-semantics trap as an Agent Note.
The Models section now declares two SlotMap seats for out-of-tree plugins:
settings.models.provider-card (keyed by the row's settingsNs, dispatched on
saved cards, the first-run setup posture, and the add-provider draft, with
the row view, configured join, and confirmed api-key state as owner props)
and settings.models.footer (ordered list after the rows and add controls).
Without registrants both seats render nothing. First consumer: the
llm-pi-ai-oauth companion plugin's sign-in surface.
`import.meta.resolve`'s `parentURL` argument takes effect only under
`--experimental-import-meta-resolve`, which no launch passes, so the
fallback resolved from this module rather than from the harness — the one
question it existed to answer. The disk walk is the whole answer now, and
the refusal memo it needed goes with it. A `file:` URL joins the file
branch rather than the package one, where a resolver would only normalize
it and report a missing target as present, and a row is skipped on the
Loader's own `Boolean(disabled)` so `disabled: 0` is checked like the
Loader checks it.
A broken card says so through `aria-disabled` rather than `disabled`, and
refuses the pick in its own handler. `disabled` took it out of the tab
order, which with the reason moved onto the badge left it unreachable
without a pointer — reachable before this change, so hiding it was a
regression rather than a path that never existed.
Both notes this decision partly supersedes are updated in place and
cross-linked, one README pair loses an editing residue that repeated a
sentence, and the single-row diagnostic no longer reads "row row 1".
- permission-presets: register the permissions unit synchronously before the
existing-session sweep, so a remount reads folded knob state instead of
treating every session as fresh; add a regression test for that path
- sandbox-policy/terminal-bash: mount the projection registry in the 5 pwsh
terminal tests, the sdk-minimal bundle, and the e2b fixture composition;
declare the new package dependency in both manifests
- plan-mode: restore .strict() on the plan unit state schema and drop the
deleted foldPlanMode from the bilingual READMEs
- session-title/session-projection: sync bilingual READMEs to the mandatory
projection seam and re-record translation pairing
- docs: turnBoundary reader contract, subagent schema comment, token-meter
import comment, sandbox-policy module docstring
A running turn disables the composer by flipping contenteditable to
false on the same element. fill() throws there immediately — a disabled
textarea used to hold it back through actionability — and isEnabled()
reports true for a div regardless, so the permission-policy scenario's
post-settle wait never waited and its next gesture raced the re-enable
render. The window is a few frames wide; #3083's Remote-routed subagent
control stretches settle enough to hit it on CI.
writeComposerDraft now waits for contenteditable="true" before acting,
and the permission-policy scenario drives all four sends through it with
the settle wait pinned to the attribute.
Discovery proved only that a composition parsed, so a preset naming a
package a later rename took away kept a healthy card and its place in
every picker until a person switched to it. It now resolves each row it
can prove will start, reading the package off disk and falling back to
the resolver only for names that look absent — the resolver costs a
synchronous hooks-thread round-trip under the source launch's tsx hook,
which the walk avoids for every row it clears.
The mount diagnostic followed `AggregateError.errors` but never a cause,
so a group that failed on two rows named neither. It now follows a cause
that carries more than its own message.
A refused switch left the chip's label snapping back with no account of
why, which is the only account there can be for a preset that resolves
and then refuses. It announces through the shared Toast, which gained a
caller-set hold for a cause that names packages and rows.
The tenth master merge staged the correct 0.3.241 notices, but the
pre-commit regenerator ran against a local pnpm store that still held a
stale 0.3.220 SDK directory alphabetically ahead of it and silently
committed the old payload table. Regenerated after removing the stale
store entries; the lexical rows this branch adds stay.
The generator names the first matching virtual-store directory, so a local
store still holding an older SDK payload alongside the locked one renders
that older version into the notices.
The full-corpus transform gate spawns one child that transforms and imports
every built bundle, so it runs for 8-25 minutes as a single case and
dominates one native Windows coverage partition, blowing its 900s budget
under load. Move it to the coverage-exempt heavy gate, which runs it with
its own worker budget instead of competing with the instrumented
partitions. The package's src is threshold-excluded in vitest.config.ts, so
the exemption carries no coverage; the exempt-heavy roster note records the
entry.
The self-hosted Windows coverage pool (16 shards x 12 workers on 192
threads) pushes real subprocess boots past their vitest deadlines: the
tool-pwsh Loader smoke reaches ~40s against a 30s process cap, and the
tool-ralph worker-thread cases exceed the 5s default. Give the pwsh
smoke a 90s process deadline (the subprocess keeps the assembled boot,
the vitest deadline stays at 120s), and give the two un-budgeted ralph
cases 30s each, matching the existing 20s quiescence case.
On Linux, pwsh may trap SIGTERM and exit cleanly when the subprocess service
is disposed, so the handle status is 'completed' rather than 'killed'. The
test already proved the process tree is gone via kill(pid,0); both statuses
satisfy the contract.
Nothing under packages/host/apiproxy imports @deepseek-ai/dsh-goal after the
unary domain deletion, so the dependency and its project reference go too.
The goal domain has been served by GoalService's @Remote namespace since it
shipped; the API Proxy copy was a second implementation of the same six
mutations. Remove the goals contract, schemas, route rows, IApiClient stub,
host implementation, and the fixture's compatibility face, leaving
ctx.remote.goals as the only path.
The fixture's goal fold keeps its coverage through the Goal Remotes: its
lifecycle case moves out of the unary-dispatch test, which no longer has
goal rows to cover.
Drop the width handle's double-click reset and tooltip — the handle is now
drag-only and a stored preference is only replaced by another drag — and
add a tertiary description line under the Settings font-size title stating
the size only affects conversation content. Update both Agent Notes and
the settings dialog goldens.
Adopt --dsh-content-font-size / --dsh-content-font-delta across the flow
rows around the transcript body: DisclosureRow header (row height, title,
leading box, and registered glyphs, with StateDot exempt), ToolRow and
bash-row summaries and file links, think text, compaction/context/retry/
error rows, message clock and icon actions, the workflow-run panel, and
the workspace browser. Update the font-size Agent Note and add CSS-text
specs for the new adoptions.
The scenario landed on master with a textarea locator; the composer is a
Lexical contenteditable surface here, so the fill waited 30s for a node
that never exists. Use the shared per-key draft helper against
[data-composer-input] like the other composer scenarios.
Other PRs are blocked by Windows ACP half-close tests timing out. Keep the
coverage job running for signal, but remove it from all-checks-passed.needs
until the Windows skip fix is validated.
Windows anonymous pipes do not surface a child stdout EOF while the child
process stays alive. The three tests that simulate 'child closes protocol but
stays alive' therefore cannot be reproduced on Windows and hang until the
test timeout. Skip them on win32.
These tests spawn real ACP child subprocesses. On contended self-hosted
Windows runners the default 30s budget times out. Instead of raising the
global coverage timeout, give this file 2x the configured default
(DSH_COVERAGE_TEST_TIMEOUT_MS) so it follows future default changes.
After the 4-partition split, other PRs' windows coverage now fails on
process-bound subagent-acp tests timing out at 30s under self-hosted
concurrency. Give the coverage lane the same 60s per-test budget that the
earlier failover profile used.
The rail's items now ride the Chat snapshot: a structural upsert re-derives
the loaded Turn set, a content-only upsert re-derives only the Turns whose
nodes changed, and each preview is capped so navigation state never holds a
copy of the transcript. The published array keeps its identity until an item
changes, so ChatView selects it as both data and change signal — and a
streaming reply's preview follows the in-place node update instead of the
last structural publication.
A scroll frame resolves the active mark with one hit test at the reading
line, falling back to a single row scan, rather than a DOM query per mark.
Flow-height changes resync through the existing column observer, navigating
during a pending page keeps the paging anchor, and the rail height no longer
holds a floor taller than the band it centers in.
Render reconstructable system prompts at each request-series boundary, preserve series declarations through pre-step wrappers, and keep the presentation and replay snapshots aligned across clients.
Keep five non-blank rows stable while the selected blank New Session is provisional, and derive the overflow count from the rows still hidden.
Fixes#2841
The rail is a landmark on every Chat wide enough to show it, so each
recorded conversation with at least two loaded Turns now carries the
navigation node and its marks.
The concurrent-migrator test wrote the winner document with a plain
writeFile, whose truncate-then-write window lets the boot's unlocked
initial read observe an empty file and boot an empty store under load.
Seed fixtures through writeFileAtomic instead, matching how the provider
itself persists, so a reader sees either the old or the new complete
document.
ChatView derives one navigation mark per currently loaded Turn, keyed by
Turn number and anchored on that Turn's first loaded user node. The rail
sits against the scrollport's right edge, centered in the band the sticky
composer leaves visible; hover and keyboard focus preview the Turn's
prompt and response, and activating a mark moves the shared scrollport
and records the resulting restoration anchor.
ConversationRoot publishes --dsh-conversation-viewport-height beside the
composer height it already measures on the scrollport, so floating View
chrome can center in that band without assuming a Session header height.
The PR changes native Windows coverage partitions from 8 to 4 to reduce
vitest worker startup pressure under high self-hosted concurrency. Sync the
implemented Agent Note (EN/ZH) so the decision record no longer says Windows
is fixed at 8, and revise the same-partition-count alternative accordingly.
Keep recognized code fences syntax-highlighted while assistant text streams. Preserve completed Shiki token lines across chunks, mirror token styles and CRLF handling, and retain plain rendering for unsupported or math-like fences.
Add unit, DOM-parity, and keyless assembled-Web coverage for the streaming-to-settled transition.
Closes#1499
Keep the 4-partition coverage profile, split the monolithic windows-native
job into smaller required jobs (build, coverage, native-tests) plus a
non-blocking observational job. Update ci-workflow.spec for the new topology.
Under high self-hosted concurrency, 8 partitions per Windows native job
triggered vitest fork worker startup timeouts. This branch lowers Windows
coverage to the same 4 partitions Linux uses, trading some single-job
coverage wall time for lower process-creation pressure.
CI hit the same dropped-fill race in lifecycle-chrome's slash-menu
sequence that the folder scenario hit: fill()'s single-task select-all +
edit lands on a Lexical selection that has not absorbed the DOM
selection after a trigger-menu interaction, so the previous draft
survives and poisons the next test. Promote the per-key gesture to
support.ts and use it at both proven-fragile sites.
The base-mounted projection cache's write-behind forces session-log flushes
at cache-chosen times, splitting packed chunk rows and changing the durable
JSONL batching the keyless headless and sdk replay fixtures pin. Neither
profile exposes a session-listing surface — the one-shot runner and the SDK
protocol — so both bundles disable the base row, restoring the pre-base
behavior the fixtures were recorded against (the same pattern acp-app uses).
The pinned prompts were recorded on master before this branch's
file-reference guidance rewrite; refresh them so the pin carries the
directory-aware wording the assembly now produces.
Directly after a chip deletion, fill()'s single-task select-all +
insertText lands on a Lexical selection that has not absorbed the DOM
selection yet and is dropped, leaving the previous draft in place. Real
keystrokes leave room for selectionchange between keys, matching what a
user's typing does.
The Windows x64 installed-wheel job timed out while waiting for initialize even though the same head passed on rerun. Exact packaged-runtime VM evidence showed a 6.47-second first cold handshake and 2.69-2.94-second warm fresh-home handshakes, leaving too little variance below the public 10-second default.\n\nRaise the independent initialize default to 30 seconds in both Python SDK configuration layers. Ordinary turn and shutdown timeouts remain unchanged, callers retain an explicit override, and tests plus paired documentation pin the public behavior.
Run the GitHub Windows runtime leg under the runner’s native PowerShell instead of inheriting the POSIX Bash body. POSIX and Windows now own explicit output resolution, virtual-environment setup, environment scrubbing, and keyless/live black-box commands, while portable build commands continue to use each runner’s default shell.
Put the pinned uv installation on the GitLab Windows job PATH before either the smoke or release builder invokes it. Reject a runtime executable whose basename does not match the selected platform manifest, and reject Intel macOS at platform selection instead of reporting a misleading missing artifact.
Add a complete PowerShell path to the published Python tutorial and record the three-phase shutdown-time bound in the Windows runtime decision. Workflow, Python, and bilingual documentation tests pin the resulting behavior.
Record win-x64 as the sole Windows Python carrier: node24-win-x64 builds a py3-none-win_amd64 wheel with dsh.exe, rg.exe, and both ConPTY addons; Windows arm64 remains explicitly unsupported. The note also pins native build ownership, shell-free pnpm launch, installed-wheel keyless/live gates, and the PowerShell-specific minimal snapshot.
Update the active SEA, sole-launcher, profile-runtime, installed-wheel, and publication decisions from three runtime wheels to four, preserving their existing rationale while linking the Windows extension. Contributor and runtime references now state the exact target, filenames, sidecars, snapshot ownership, and five-wheel release set in both languages.
Make the checked-in minimal SDK overlay disable both one-shot shell rows and mount exactly one persistent PTY stack: Bash on Linux/macOS and PowerShell on Windows. The SDK server, explicit dsh home, persistence, editor, timeout, and reduced tool catalog remain unchanged.
Update the runnable example and tutorial to list Windows x64 as supported, describe the platform-selected shell, and remove the obsolete POSIX-only restriction. This keeps the documented first Python task executable through the packaged Windows dsh profile instead of advertising a Linux-only overlay on a Windows-capable SDK.
Add node24-win-x64 to the required pull-request and public-release matrices on a native windows-2025 runner, and publish the same win_amd64 artifact from the GitLab tag pipeline. GitHub uses Git Bash for the shared release script while selecting the Windows venv's Scripts/python.exe explicitly; the Linux and macOS legs retain their existing commands and native checks.
Run the complete installed-wheel keyless suite and the trusted two-turn DeepSeek smoke on Windows exactly as on the existing targets. Make the minimal blackbox choose persistent PowerShell on Windows, keep advanced and restart snapshots platform-stable by disabling both one-shot shell variants, locate the generated dsh.exe console command, and validate text lines without assuming POSIX newlines.
Workflow tests pin the four-target matrix, Windows runner and wheel tag, cross-platform venv selection, GitLab publication dependency, and full blackbox invocation. The existing POSIX minimal snapshot changes only its platform-neutral prompt wording; Windows owns a separate model-visible snapshot.
Add node24-win-x64 as the only supported Windows runtime target and publish it as a py3-none-win_amd64 wheel containing the conventional dsh and ripgrep .exe payload names. Keep Windows ARM64 rejected explicitly so Python cannot claim a carrier that CI and release automation do not build.
Teach the pkg builder to require a native x64 Windows host, validate both node-pty ConPTY addons, copy @vscode's win32 ripgrep executable, and recognize pkg's .exe output. Extend runtime resolution, wheel staging, payload validation, and the preset closure check so the Windows-specific PowerShell plugins and sidecars fail loud when omitted.
The sidecar resolver now maps a packaged main.exe to main-rg.exe; focused TypeScript and Python tests cover that name, the win_amd64 manifest, x64-only host selection, complete wheel payload, ConPTY inventory, and platform-conditioned plugin closure.
The keyless max-token smoke carried a disable overlay that reproduced the removed private carrier's reduced tool roster. That legacy roster is now owned by the standalone sdk-minimal profile, so mutating the full sdk profile hides the application the SDK actually ships.\n\nLaunch sdk without a patch for both the successful and invalid-config paths. The separate sdk-minimal process test continues to pin its exact platform-selected two-tool request.
The startup test helper always constructed an explicit sdk profile, so coverage never exercised apply's supported default-config path even though runtime behavior depended on it.\n\nPass Config objects through the helper and default them to an empty config. The existing startup and help tests now prove sdk defaulting, while the explicit sdk-minimal case remains covered.
Stop narrowing the standalone DeepSeek adapter to a DSH_MODEL-derived one-entry catalog. The direct adapter already accepts model ids outside its advisory catalog, so retain only the DSH_CONTEXT_WINDOW fallback and let the JSON-RPC initialize model be the single runtime selection.
Remove model mirroring from minimal.py and the packaged smoke. The keyless process now initializes deepseek-v4-pro without DSH_MODEL, while the packaged scenario continues to use its unlisted smoke-model; together they prove both cataloged and arbitrary SDK model arguments reach the adapter directly.
Update the bundle, tutorial, SDK/example references, and owning Agent Notes to keep DSH_MODEL only as minimal.py's optional default input, never as a second value callers must synchronize.
Record sdk-minimal as the narrow repository-owned exception to base-first profile composition: callers still launch only dsh and cannot provide an arbitrary Cordis tree, while the shipped bundle may own a complete explicit roster. Cross-link the launcher, profile-bundle, Python-runtime, minimal-agent, snapshot, and telemetry decisions; the supersession audit keeps each older note active because its remaining rationale is independent.
Update the CLI, architecture, Python tutorial/reference, example, runtime-wheel reference, and bundle documentation. The docs distinguish the full sdk profile from sdk-minimal, explain explicit-home/plugin/patch customization, state the minimal permission and persistence choices, and retain the separately packaged web profile and frontend assets for direct dsh use.
Correct dsh-base descriptions to cover base-backed profiles, make SDK startup configuration visible in the generated config catalog, add sdk-minimal to the module graph, and regenerate the base-composition graph. English and Chinese pairs are re-recorded at the exact reviewed contents.
Make minimal.py select the shipped sdk-minimal profile directly and pass its selected model into the profile-owned adapter catalog. The Python SDK still starts only the bundled dsh CLI with an explicit Harness home; it no longer supplies an invocation overlay for this mode.
Drive both the source keyless process test and installed-wheel smoke through the same named profile. The keyless test pins the generated profile manifest and exact two-tool model request, while the packaged smoke keeps the persistent-shell, editor, session-log, and model-visible snapshot evidence.
Delete minimal.patch.yml and the unused complete-config/replay fixtures. Their composition now has one owner in @deepseek-ai/dsh-sdk-minimal, so the example and tests cannot drift into separate launch trees.
Add a startup-only sdk-minimal template whose sole bundle inserts the complete JSON-RPC agent tree over the empty profile root. The roster is an explicit composition allowlist: it contains one DeepSeek adapter, the minimal agent spine, persistent Bash, the string-replace editor, local execution, and JSONL persistence, while dsh-base and Web remain absent.
Reuse the SDK app startup provider so the new profile retains help, stdin EOF, and bounded launcher shutdown semantics. Make that provider render its configured profile name, which keeps both sdk and sdk-minimal help truthful without duplicating process lifecycle code.
Register the package in the CLI closure, TypeScript graph, lockfile, Knip policy, and bilingual bundle references. Exact manifest, row-roster, profile-template, config-dump, and HMR tests make later additions visible instead of relying on a blacklist.
Resolve the installation fallback generation before locking and return immediately when every required symlink or packaged proxy is complete. Parallel SDK rollouts sharing an initialized DSH_HOME therefore do not queue on profiles/node_modules.lock.
Missing or stale entries still acquire the cross-process writer lock, recheck the generation, and repair under exclusive ownership. Tests hold the lock to prove the steady-state bypass and verify that a partial repair retains already-correct siblings.
Give the SDK JSON-RPC server a per-root tool filter and let deployments mark the configured persona as the complete system prompt. The checked-in minimal overlay now names only bash and str_replace_editor, so later global tools and unrelated guidance from dsh-base cannot appear implicitly.
Keep the shared SDK host services and packaged Web capability intact. Only workspace instructions, compaction, and the conflicting one-shot Bash row remain disabled. Unit coverage pins the configuration paths, and a real dsh profile smoke proves the assembled prompt and exact two-tool request.
The packaged dsh launcher must expose installation modules to profile-local plugins without writing symlinks into pkg's virtual filesystem. The first review fix selected ESM exports correctly in ordinary Node, but real carrier execution exposed package metadata and VFS behavior that a synthetic tree did not cover: executable and declaration packages have no import entry, legacy main fields rely on Node probing, and pkg's Windows VFS prevents filesystem package-scope resolution from seeing exports such as zod/mini and @google/genai/web.
Resolve explicit exports directly from each installed manifest with the maintained resolve.exports package under Node import conditions. Publish only package-local candidate files that exist, reject escaping or malformed targets, preserve the package installation URL without realpath, and keep Node's legacy resolver only for exports-less packages. This avoids pkg filesystem package lookup entirely while retaining fail-loud behavior for broken runtime entries.
Add regression coverage for import-only, nested, symlinked, zod-style, and genai-style condition maps; unavailable and types-only entries; invalid and escaping targets; executable/declaration packages; extensionless main; and legacy index fallback. profile.ts remains at 100% statements, branches, functions, and lines. Update the bilingual package and Agent Note contracts, replace the runtime dependency and generated notice, and regenerate the lockfile through pnpm.
Resolve packaged profile proxies with Node ESM import conditions from each package installation, and fail loud when an explicit runtime export or legacy main entry is missing. Serialize the shared profile fallback under the existing cross-process writer lock so concurrent dsh processes cannot observe partial proxies; either carrier now replaces the other carrier’s managed entry without manual cleanup.
Give Python initialize its own 10-second default bound and name the selected profile in timeout diagnostics, while leaving ordinary agent turns unbounded by default. Package the dynamically resolved web frontend and skill-badge assets so the runtime wheel’s normal dsh profiles do not depend on pkg static-discovery accidents.
Rewrite the root launch rule and every active stale SDK-runtime note to the shipped dsh profile architecture in both languages. Focused tests prove import-only and transitive package exports, lock contention, cross-carrier transitions, missing-entry failures, asset inventory, and bounded initialization.
Document dsh as the only application launcher across architecture, CLI, SDK, app-boot, Python package, contributor, tutorial, and example references. Explain explicit home selection, profile and patch precedence, persistent external plugin installation, the Node-free runtime path, and the absence of complete-config or ~/.dsh fallbacks.
Record the Python profile-runtime decision and update the active naming, installed-wheel, and SEA packaging notes with precise supersession. Regenerate the configuration catalog and module graph after deleting the carrier, update both reviewed languages and pairing records, and classify the retained standalone Cordis files as lower-level test fixtures rather than launch interfaces.
Migrate the packaged-runtime smoke inventory from complete Cordis trees to the sdk profile plus ordered patches. Preserve the focused minimal and advanced behaviors, update the generated durable snapshots for explicit permission events and the smaller RunResult, and keep worker, MCP, ripgrep, PTY/editor, direct JSON-RPC, and real-provider coverage.
Add an installed-only external bundle scenario that invokes the wheel's dsh plugin command with a local file package, verifies profile manifest reconciliation, imports @deepseek-ai/cordis as a peer, asserts the packaged proxy returns the exact host Context instance, and proves its system-prompt contribution reaches the model. Migrate the repository source e2e and runnable minimal example to the same profile grammar.
Replace complete-config, session_root, runtime-bin, bridge-bin, and public argv override options with dsh_bin, profile, ordered patches, and dsh_home. Resolve executable/home/patch/cwd paths before spawn, select the sdk profile by default, and fail before launch unless dsh_home or non-empty DSH_HOME is explicit; Python never inherits ~/.dsh silently.
Remove Python-owned DSH_CORDIS_CONFIG, DSH_SESSION_ROOT, and DSH_CWD injection and drop session_root from RunResult. Keep arbitrary argv only as an underscore-prefixed fake-runtime adapter, retain provider/model/token and process controls, and append subprocess stderr to initialization JSON-RPC errors so profile boot failures name their actual plugin cause. Unit and carrier tests cover both exe and Node modes.
Make the zero-code dsh-python-runtime-closure depend on the real @deepseek-ai/dsh application and every required profile peer, then package apps/cli's built bin instead of the deleted Python carrier. Rename executables to deepseek-harness-sdk-runtime-<platform>-<arch>, update wheel/platform/build workflow discovery, and install a Python dsh console command that requires explicit DSH_HOME before exec.
Include profile, bundle, preset, native addon, and shared-library assets needed by the full CLI. Remove the checked-in default cordis.yml and preserve the existing wheel distribution names, Python module names, sidecar validation, and wire identity. Runtime resolution and release tests pin the new artifacts and dev Node carrier.
Teach the profile installation fallback to use normal symlinks under Node and real ESM proxy packages under pkg. Each proxy records the source package version, mirrors its explicit runtime subpath exports, and re-exports the virtual /snapshot URLs, so built-in Loader rows and external plugin peers resolve one shared Cordis/module instance from an on-disk profile.
Keep proxy healing idempotent, reject foreign real directories, cover root and subpath imports in packaged mode, and expand AggregateError startup diagnostics so concurrent Loader failures retain their individual import causes. This is the reusable packaged-profile mechanism; Python-specific artifact wiring remains in the next commit.
Delete @deepseek-ai/dsh-sdk-python-runtime and its packaged-bin entry now that Python uses the repository's dsh application launcher. Remove the package's project reference, Knip entry, workspace-policy exception, executable allowlist entry, and README-model classification together so no tooling preserves the old exception.
This commit is deliberately mechanical: it removes the obsolete package and gate accommodations without introducing the replacement launch behavior. The following commits add the packaged dsh runtime and Python profile API, keeping the architecture change separate from deletion noise.
The read-image-reencode lane feeds a 16-bit gradient PNG through the
shipped app: pass-through is impossible, the master converts down the
opaque JPEG ladder, and the 640,000-pixel request budget re-encodes a
downscaled request version — the projection the byte-identical tiny
fixtures never exercised.
Master dimensions move from a 2048 long-edge rule to a total-pixel
budget (normalizedImageMaxPixels, default 2048x2048) with an 8192
long-edge cap, so extreme aspect ratios keep short-edge resolution.
The shared quality ladder and lazy execution move to encoding.ts,
review-round doc fixes land across attachment and llm packages, and
the superseded facts in the unified-image-pipeline note now describe
the shipped routing.
Field test: the model received a bare '@niulai/' and guessed a user
mention — the guidance section was present but said only that @-prefixed
paths are files, never covering the trailing-slash directory form, the
workspace-relative root, or the quoted spelling. Rewrite the section to
name all three; the section's presence conditions and every consumer
pin the constant, so nothing else moves.
CI's snapshots lane timed out waiting for the folderx candidate: the
fixture directory was created after connectFreshWorkspace, racing the
Host's file index on a loaded runner. Land every fixture (and the
workspace directory itself) before the connect, and give the first
folder query a cold-start allowance.
A directory row in the @ menu had one verb doing two jobs: picking it
inserted literal @dir/ text and kept the menu open, so a user wanting
the folder itself never got a settled entity — the token kept its
trigger character and stayed editable, nothing like a file's atomic
chip.
Split the intents on the same row, mapped to shell-completion instincts:
row click / Enter settles the directory as an atomic folder chip (the
file chip's exact language; canonical @dir/ mention as its serialized
form — the { insert } arm the folder path never took), while Tab or the
row's trailing chevron drills: literal editable text, menu open on the
children. One new dimension carries it: candidate.drill advertises the
verb, InputTriggerPick.action reports it, ArbitrateKey gains 'tab', and
the keymap intercepts Tab only while a drill row is highlighted.
Covered by controller arbitration, MenuView chevron routing, the
ui-reference verb split, a keymap Tab-passthrough spec, and a real-
browser e2e driving all three gestures; menu golden refreshed for the
chevron and the fixture directory.
foldSurfaceTokens rebuilt the priced surface on every surface event: an
append allocated [...nodes, node] and a replacement copied the whole
array before splicing, charging every well-formed event O(surface) for
an atomicity property only malformed events need. Benchmarks put the
copy at ~99.9% of an append's cost (100µs at a 50k-node surface vs
0.1µs for pricing) with O(S²) accumulation over a session, inside the
synchronous session/event publication path.
Split the fold into the session core's planSurfaceEvent/applySurfacePlan
shape: planSurfaceTokens performs every fallible step against the
read-only surface, commitSurfaceTokens applies the plan in place and is
infallible by construction. _foldEvent plans first, runs the remaining
fallible anchor validation, and only then commits, so retry identity is
preserved by ordering instead of by allocation. Appends drop to
amortized O(1) (100.3µs -> 1.9µs at 50k nodes); replacements keep their
O(surface) findIndex but stop paying the extra full copy (21µs -> 4.2µs).
A new regression test pins the one hazard this introduces: an event
whose surface plan is valid but whose later anchor validation throws
must leave the priced surface and running total uncommitted across
repeated failures.
- The base-mounted projection cache's write-behind forces session-log
flushes at cache-chosen times, splitting packed chunk rows and collapsing
the persistence window the keyless acp transcripts pin. ACP exposes no
session-listing surface, so the automation profile disables the base row,
restoring the pre-base behavior the fixtures were recorded against.
- The windows coverage lane runs suites that read built lib/ output (the
webworker-packer image tests) and raced the build gate's tsdown writes
against a partial tree; every coverage gate now waits for the build gate.
Delete the 5-bit colour-count classifier and palette PNG branch that
misrouted high-frequency photographic JPEGs (issue #2885 images 23/24)
into an encoder 100x slower with 4x larger output. Both normalization
and request-image encoding now route by the decoded alpha fact alone:
opaque sources down a JPEG ladder and alpha sources down a WebP
effort-0 ladder, each at qualities 85/75/60. Byte budgets become ladder
targets: the downscale retry loop is gone and a ladder-exhausted encode
keeps its smallest output, while provider byte caps stay enforced at
the transmitting route. Request transforms move to request-image-v5.
The exact-shape fix added two filesystem-failure catch branches that only
fire on a race/permission fault the caller already guards against (the
session-dir readdir after an isDirectory() check, and the discovered-root
rmdir after the root was observed empty). Neither is deterministically
reproducible in-process, so tag both with the same reasoned v8 ignore the
sibling catch blocks already use, restoring per-file 100% coverage and the
symmetry between the parallel rmdir handlers.
Tighten the startup sweep to backend-generated name shapes and fix the tests
that had drifted from the SweepRoot-based API:
- Match roots by the exact `dsh-spill-<6>` mkdtemp shape and session dirs by
`session-<12 hex>` (DEFAULT_ROOT_RE / SESSION_DIR_RE), replacing loose
startsWith checks so foreign or fixture-shaped directories are never swept.
- Carry `SweepRoot { path, pruneWhenEmpty }` through SweepOptions so a discovered
prior-default root is removed once emptied while the active root is never
pruned; lstat each session entry so a symlinked session dir is not followed.
- Fix the tests to the SweepRoot API: import SweepRoot, correct the gatherRoots
override return shapes, build discovery fixtures with the real mkdtemp shape,
and route the warn-wiring test through a deterministic failure path.
The local spill backend never reclaimed its files, so configured roots
grew without bound and default per-process dsh-spill-* temp roots piled
up across runs. Immediate deletion is unsafe because persisted, resumed,
and forked sessions may still reference an older locator.
Add a fiber-owned, best-effort sweep that runs once after activation
(never delaying availability, awaited on disposal): it deletes regular
files older than cleanupPeriodDays (default 30; 0 disables) across the
configured root and prior default temp roots, prunes emptied dirs, and
skips symlinks/unknown entries. Every filesystem failure is contained
and logged, so the sweep cannot fail activation or a concurrent write.
preview-boot's hero wait was master's new textarea:enabled locator (the
conv refactor predates the Lexical composer); migrate it to the
data-composer-input surface like every other lane. Drop the unnecessary
DOMRect assertion oxlint flagged on the new Range stub.
- user-text.tsx enters ui-primitives' per-file 100% gate: replace three
regex-guaranteed impossible ?? fallbacks with asserted captures, make
the precedence sort a branch-free rank comparator, and pin the nested
recall-label ordering and the no-basename quoted-path fallback with
tests (100% statements/branches/functions locally).
- assembled-boot stubs Range.prototype.getBoundingClientRect: jsdom has
no Range geometry, and Lexical's selection reveal now reaches it in the
built-graph lane after the architecture merge (the unhandled TypeError
behind command-image-envelope and preview-boot).
The worker-preview pack landed on master declaring dsh.configTrees
against apps/cli/config/agent-presets, which this branch moved into
packages/preset/agent-presets/presets. The VFS mount and the worker-side
roster patch keep their paths; only the source directory follows the
move.
The spec landed on master before the roster gained the plugin-bundled
shipped root; its empty-roots harness now opts out explicitly, matching
every other exact-roster suite.
The Landlock packed-install rehearsal packed a hand-maintained list of
workspace tarballs. When dsh-llm gained the dsh-util-crypto runtime
dependency, the list stayed stale and npm tried to fetch the unpublished
release candidate from the public registry, failing both Linux master jobs
with E404 before confinement ran.
Read the current pnpm workspace inventory and traverse dependencies,
optionalDependencies, and required peerDependencies from the packed test
roots. Verify package identities, fail loudly on unresolved workspace names,
sort the closure deterministically, and leave native-family packages to the
existing mode-preserving native packer.
Cover runtime traversal, optional-peer exclusion, native filtering, and
invalid workspace metadata. Remove the obsolete vendoring exact edit for the
deleted manual list so future runtime workspace additions are included by
their manifests instead of becoming post-merge CI failures.
Unix PowerShell emits cursor-position requests while PSReadLine starts and
redraws prompts. The subprocess PTY is only a transport, so those requests
went unanswered. Startup could then accept the dsh> literal echoed from its
setup source as a rendered prompt, and later sends were lost or clipped.
Feed raw PTY output into a zero-scrollback @xterm/headless state machine and
write generated replies through the provider-owned terminal handle. Drain
replies before caller input, repeat foreground inspection when terminal
activity races the sample, and retain send ownership until parser and reply
work quiesce. Coalesce raw chunks behind one active parser write so large
Windows output cannot create thousands of queued parse callbacks.
Publish pwsh only from backend stdin_read evidence and start one timeoutMs
deadline before the complete startup retry loop, so inferred-idle follow-ups
cannot reset the bound. Dispose the emulator when the
terminal or cleanup fails. Document the fail-loud ConstrainedLanguage path
and add focused coverage for split queries, reply ordering, foreground
resampling, failure containment, batching, timeout, and disposal.
* test(python): exercise installed wheels as black boxes
Add an installed-wheel mode that refuses source/editable imports, repository working directories, mismatched SDK/runtime versions, unpinned runtime dependencies, and executables outside the installed runtime distribution. The mode resolves the wheel-owned executable itself, so callers cannot accidentally prove an explicit checkout artifact.
Add a real-API scenario that drives two tool-using turns through the public synchronous SDK, verifies the file bytes outside the agent, checks completed turn/tool events and persistence, and projects provider failures without retaining credential-bearing error text. The existing deterministic scenario set remains the keyless behavior oracle.
Refs #2952.
* ci(python): require installed-wheel checks on every release target
Move the complete deterministic runtime scenarios behind construction and clean installation of the SDK and matching runtime wheels. Each native leg runs outside the checkout with source-resolution environment variables removed; Linux manylinux smokes assert the same installed provenance.
Expand the required pull-request call from Linux x64 to Linux x64, Linux arm64, and macOS arm64. Trusted heads receive only DEEPSEEK_API_KEY_EXTERNAL for a fail-loud live two-turn smoke on each carrier, while fork and Dependabot heads retain the full keyless path without exposing secrets.
Pin the reusable secret declaration, matrix call, aggregate dependency, untrusted-head condition, and live/keyless commands in the workflow contract test.
Refs #2952.
* docs(testing): make installed wheels the Python CI authority
Record the clean-wheel provenance boundary, complete keyless scenario set, trusted real-API contract, secret handling, and three-target required topology in a new implemented testing decision. Update the SEA distribution and portable-CI authorities plus the Python contributor reference to describe the same current state.
Archive the fully superseded Linux-x64-only decision after consolidating its rationale and alternatives into the new owner. Preserve its bilingual triplet as a sealed historical snapshot and redirect every active current-state reference.
Refs #2952.
Move the shared module-reload policy into dsh-base by inserting its HMR row disabled, then remove the redundant disabled overrides from Web, headless, SDK, and ACP. No shipped profile enables server module reload; live profile patch watching continues through the launcher-owned config-only fallback, and browser client HMR remains a separate mechanism.
A later profile layer can opt into source-module reload explicitly with disabled: false while retaining the base root configuration. Composition tests cover every shipped mode and the explicit enable path, and the bundle references plus launcher Agent Notes document the resulting ownership and safety rationale.
Set DSH_E2E_MAX_WORKERS=4 for the credentialed e2e workflow and pin that environment contract in the workflow test. Profile-launched SDK and ACP scenarios each boot a complete subprocess tree, so the previous file-level fan-out could multiply process and provider pressure far beyond the runner's useful concurrency.
The bound changes scheduling only: every e2e file still runs, the Vitest configuration retains its explicit override knob, and local callers can choose a different positive worker count when their resources allow it.
Regenerate the configuration catalog and module graph after replacing standalone application packages with dsh profile bundles and renaming the private Python carrier. Update built-bin coverage to launch the shipped sdk and acp profiles, assert retired bins stay absent, and keep the Web golden text aligned with the same assembled runtime.
Clarify that headless is a startup profile whose patches freeze after boot. This commit is projection and verification work: it contains no application implementation, and every generated document is produced from source committed earlier in the series.
Update workspace manifests, the lockfile, Host project references, Knip inputs, package constraints, vendoring rewrites, and Python runtime build/smoke scripts for sdk-app, acp-app, and @deepseek-ai/dsh-sdk-python-runtime. Add the ACP hook packages to the dsh dependency closure so installed profile materialization resolves the same plugins as source workspaces.
Keep Python distribution outputs deliberately unchanged: the wheel modules, executable names, and smoke targets retain their public identities even though their private npm carrier moved. Constraint fixtures pin the new package locations and catch missing application dependencies on every platform.
Add verify-application-entrypoints to the top-level gate graph. It inventories executable sources and package bins across apps, packages, and examples; rejects unclassified launchers including root-level js/mjs/cjs/ts files; and permits only the dsh CLI plus the explicitly private Python runtime carrier exception.
Update repository, architecture, CLI, and naming records to state the same rule: Node consumers select a dsh profile instead of invoking application-package bins, and no compatibility aliases remain. Fixtures prove both allowed classifications and representative escape attempts, making the architectural rule mechanically enforceable.
Rename the relocated npm workspace to @deepseek-ai/dsh-sdk-python-runtime and the runnable example to python-sdk-agent, then update every owning English/Chinese document, test, and packaged-runtime reference. Remove the obsolete standalone bin while retaining the carrier entrypoints used to assemble the wheel runtime.
This is intentionally a naming and ownership change, not a Python SDK migration. The public deepseek_harness_sdk and deepseek_harness_runtime module, wheel, executable, environment, and wire behavior remain unchanged. Documentation records that this private carrier is the temporary sole non-dsh application exception and will move to profile launch later.
Regenerate the four TypeScript SDK replay scenarios after switching their subprocess to dsh --profile sdk. The fixtures now project profile-owned runtime context, tool composition, nested-child persistence, and the opt-in DeepSeek session-log acceptance event while preserving each scenario's final response and file assertions.
This commit contains only committed snapshot outputs. Separating them from the SDK implementation keeps protocol/API review focused and makes the model-visible consequences auditable as generated evidence.
Replace the TypeScript SDK's public arbitrary command/argv launch surface with the same-version dsh CLI, a named profile, ordered per-launch patches, optional process cwd, and explicit Harness home selection. Installed consumers use the built CLI; clean source checkouts use the package's src/bin.ts through an absolute tsx/esm loader and a source-only patch that omits build-generated Typert loading.
Materialize explicit or inherited environments at spawn time, keep profile-internal patches below caller patches, and resolve every caller-relative path before the child starts. The SDK subagent provider validates its optional CLI and patch files at plugin load and requires an isolated absolute child home.
Treat JSON-RPC initialize as the Loader-owned readiness point: Loader settlement joins entry imports, fiber lifecycle work, and synchronous effect registration, so the server needs no scheduler tick. A delayed profile entry registers a private adapter before initialize resolves, proving caller-supplied plugin routes are visible without fallback.
Update sdk-app ownership, server diagnostics, TypeScript SDK examples, nested-loader coverage, and session-upload replay layering together. The following commit contains only the regenerated SDK transcripts, keeping this API and lifecycle change directly reviewable.
Move examples/jsonrpc-agent to examples/python-sdk-agent and packages/examples/jsonrpc-demo to packages/sdk/python-runtime while preserving every file byte-for-byte. The directory placement now reflects the example's Python-distribution role and the private runtime carrier's SDK ownership.
This commit deliberately keeps the old package names, commands, configuration, and snapshots inside their new directories. All 42 files are 100% renames; API/profile migration and naming changes follow separately so reviewers do not have to disentangle behavior from filesystem movement.
Regenerate the ACP composition graph and committed replay outputs from the assembled dsh base plus acp-app profile. The updated logs, system prompts, tool schemas, and stdout projections capture the same scenarios after shared plugins move out of the retired demo package and into profile-owned composition.
There is no runtime source in this commit. Keeping generator-owned artifacts separate lets reviewers validate the behavioral migration first, then inspect expected wire/model-visible consequences as a mechanical projection update.
Replace the standalone @deepseek-ai/dsh-acp-demo application with dsh --profile acp plus ordered example patches. The shipped acp-app bundle owns only the protocol bridge; every example overlay now targets shared dsh-base rows instead of copying a complete application tree.
Move launcher responsibilities into the ACP snapshot harness: it materializes profile patches, links required packages, reserves stdout for JSON-RPC, observes spawn and drain failures, and escalates process teardown deterministically. The relocated control-surface fixture and the ACP/subagent integration tests now exercise the real CLI/profile path.
This commit contains authored runtime, configuration, and test changes only. Generated transcript and projection churn is deliberately left for the next commit so reviewers can inspect the migration logic without hundreds of expected-output edits.
Move the ACP control-surface e2e, scripted LLM, and Cordis fixture out of the application package that later commits retire and into the runnable examples/acp-agent test tree. This gives the assembled dsh profile example ownership of its integration fixture.
This commit is intentionally mechanical: all three files retain their exact blobs and appear as 100% renames. Imports and launch behavior are updated only in the subsequent ACP migration commit, keeping reviewer-visible code changes separate from file movement.
Introduce @deepseek-ai/dsh-acp-app as the thin application layer for the built-in acp profile. It contributes only the ACP protocol bridge and profile metadata; dsh-base remains the single owner of shared agent composition, providers, persistence, permissions, and tools.
Wire the bundle into CLI resolution, catalogs, workspace configuration, and built-bin coverage. The focused bundle and startup tests prove that base plus acp-app exposes automation sessions while keeping stdout reserved for ACP JSON-RPC.
Introduce @deepseek-ai/dsh-sdk-app as the thin application layer for the built-in sdk profile. The bundle contributes the JSON-RPC server and startup-only profile metadata, while dsh-base continues to own the shared agent, provider, persistence, and tool composition.
Publish ctx.appReady from the launcher only after the Loader tree and launcher-owned setup succeed. The stdio lifetime binding leaves stdin unread until the protocol transport claims it and defers EOF exit 0 until readiness commits, so early protocol frames remain buffered and a racing startup failure remains the nonzero process outcome. Fiber disposal cancels both pending lifecycle listeners.
Register the bundle in the CLI resolver closure, generated configuration catalog, workspace graph, and built-bin smoke. Startup tests prove that base plus sdk-app exposes the SDK server without taking ownership of shared runtime plugins; focused and built-bin regressions cover early input, EOF readiness, and startup-error precedence.
Add a patchReload field to built-in profile metadata and carry it through CLI profile resolution into app boot. Live profiles install the existing patch watcher; startup profiles freeze every layer after boot and apply later edits only on the next launch. Missing or invalid metadata fails before the plugin tree starts.
The implementation keeps reload policy with the profile that owns it instead of inferring behavior from an entrypoint. Unit tests cover metadata validation and both lifecycle modes, while the CLI and app-boot references document which built-ins are live versus startup.
Record the final architecture before any runtime or file-layout changes. The decision makes named dsh profiles the only supported Node application launch path, gives TypeScript SDK callers ordered profile patches for per-launch customization, and retains the packaged Python runtime as an explicitly temporary exception with a later migration obligation.
Keeping this decision in a documentation-only commit gives every following commit one stable naming, lifecycle, and compatibility reference. The English and Chinese notes and their pairing record enter together.
- stop attributing the 45-case helper/Node divergence to the path port spec,
which pins only the Node-facing port to Node
- keep the divergence measurement as provenance, without a dead owner
- fix the path corpus comment so it does not claim divergence from the spec
that asserts equality
- drop the v2 generation stamp from the cordis mount fallback test
- restate the watcher refusal rationale in current-state terms
- re-record the ui-workspace bilingual pair after rebase
Remove dead design-session citations, change narration, indexical
stamps, and review-adjacent justification found by the
dsh-trim-cot-leakage recall batteries in prose that landed after the
last purge. Bilingual README pairs are re-recorded.
Config catalog, module graph, event producer-consumer tables, and
third-party notices regenerate over the webworker surface; the oxlint
rule fingerprint and the ui-renderer NodeNext import face follow.
One Vite build emits dist/index.html and dist/preview.html sharing every
chunk; the only difference is one prepended bootstrap entry whose module
connects the worker host, so the page from the stock entry onward is the
served startup chain verbatim. The dist moves to a relative base so the
preview mounts under any static directory, and the served form anchors
deep SPA-fallback paths with a rendered <base href="/">. The preview-boot
e2e serves the real built pages, packs the VFS image when absent, and
holds the boot line's lowering contract, the interactive hero, and a
clean page-error channel in headless Chromium.
The webserver renders a boot-readiness tail after the injection rows and
AppWebEntry.run awaits the __DSH_BOOT_READY__ deferred before reading any
injected state. Whichever bootstrap applies the injection table settles
the deferred - the served renderer resolves it inline, an asynchronous
bootstrap installs it ahead of the entry module and settles it with the
handshake - so both deployments run one startup chain and a failed
handshake surfaces on the boot page instead of proceeding on missing
globals.
Buy the grammar, own the execution: @yarnpkg/parsers parses the command
line - aliased at bundle time to its shell entry so the root barrel's
syml/js-yaml closure stays out of the worker - and a VFS-backed evaluator
with a coreutils command table runs it inside the worker host. Each shell
process is a real child WebWorker spawned from the same bundle (the first
frame decides the role), so the TERM-then-KILL ladder is real - TERM
requests, KILL terminates the worker - and the file face stays
asynchronous end to end, since the deployment target serves no COOP/COEP
headers and SharedArrayBuffer never exists there. node:child_process
reports through the ChildProcess surface the subprocess service consumes;
execSync, execFileSync and fork refuse, and node-pty stays stubbed.
Two private experimental packages run the whole harness tree inside one
dedicated Web Worker. dsh-experimental-webworker-runtime owns the in-memory
VFS (BigInt stats with per-path identity and strictly increasing mtimes),
the CommonJS wrapper loader over a lazily-evaluated builtin table whose
shims typecheck against Node's own module types, the postMessage tunnel
speaking plain HTTP, the AsyncLocalStorage runtime, and the worker
assembly. dsh-experimental-webworker-packer lowers every module body at
pack time against the shared wrapper contract, sweeps the profile closure
by static reachability, and writes a deterministically gzip-compressed tar
the worker inflates through the browser's native DecompressionStream while
it downloads.
The old backdrop overpainted a folder reference's trigger character with
IconFolderClose16; the Lexical rewrite kept the data-ref-appearance
attribute but nothing consumed it, so the composer showed a bare blue
token while the sent bubble carried the icon. A Lexical text node cannot
split out its trigger character for overpainting, so the glyph renders
as an icon prefix instead: a currentcolor mask of the same asset before
the intact literal token. The Lexical note's behavior line follows.
Keep the active implemented and proposed Agent Notes current with the
renamed ToolCallId brand: branded-ids, content-block-vocabulary,
approval-seam, tool-output-spill-files, and task-surface, in both
languages, with re-recorded .i18n.yaml pairing records.
Review-round hardening of canonical admission:
- passthrough now requires a single-frame source free of EXIF/XMP/IPTC
metadata, so location/device metadata never enters durable storage and
stored dimensions always describe the perceived pixels; animated WebP joins
GIF on the always-re-encode path (first frame only)
- SourceImageInfo records orientation-applied dimensions, keeping source and
stored raster on shared axes for coordinate mapping
- validateImage runs a canonical-encoding dry run, so a validated batch can no
longer be refused mid-write by the byte target (no partial writes)
- read_image names per-axis multipliers when rounding splits the two ratios
and maps IMAGE_TOO_LARGE to actionable downscale guidance
READMEs (both languages) describe the wide source envelope, the canonical
encoding and its fixed encoder parameters, and read_image's downscale
envelope; tool/config catalogs regenerate for the new schema and Config
fields. The read-image-dimension scenario now pins the acceptance the old
2000px admission cap refused: the 2001x1 source is admitted and stored
byte-identically, so the fixture stays platform-independent.
The canonical ref serializes mediaType, width, height, bytes in the order the
pre-canonicalization store used, so existing session-log fixtures and logged
histories keep byte-identical reference JSON.
When the attachment store's canonical encoding shrinks the file on disk, the
read_image envelope names the original dimensions and the multiplier that
maps coordinates measured on the attached image back onto the file, and the
output schema carries sourceWidth/sourceHeight for programmatic callers.
Admission now validates a wide source envelope (32MiB, 100MP, 16384px per
side) and persists a canonical encoding instead of refusing large sources:
EXIF orientation baked in, metadata stripped, long edge downscaled to the
configured canonical target (default 2048px), PNG palette for alpha/PNG/GIF
sources and a fixed JPEG quality ladder (85/75/60/45) until the canonical
byte target holds (default 1MiB). In-budget PNG/JPEG/WebP passes through
byte-identically so equal sources keep deduplicating to the same content
address; GIF always re-encodes to the PNG of its first frame, pinning the
first-frame meaning providers apply. Encoder parameters are fixed by design;
only the canonical budget is deployment configuration.
AttachmentStore.saveImage now resolves SavedImageAttachment: the durable
reference paired with the submitted raster's intrinsic facts, so a store may
persist a canonical re-encoding while callers keep the source dimensions for
coordinate mapping. saveImages keeps returning refs; every fake store and the
cordis API catalog follow the new signature.
The user-bubble decorator rendered every plain run through the
block-level MessageText div, so a decorated single-line message broke
into one line per run and the space between two tokens rendered as a
blank line. The queue dock's read-only row printed row.preview verbatim,
showing the wire session form (@[label](dsh-session:...)) instead of a
readable label. Both predate the Lexical composer; the logged model text
was correct in both cases.
One shared inline projection (reference/user-text.tsx) now owns sent
user text for the bubble and the queue row: plain runs are spans with
white-space policy left to the consumer (bubble pre-wrap, queue nowrap),
and a highest-precedence rule folds the wire session form to its label
chip, shielding the URI from the bare-token scan. The queue edit field
keeps the literal sent text. user-text.client.spec pins the inline
guarantee and every fold rule; queue-actions.e2e locators move to
row-container matching (the projection adds one span layer).
Exposed by this branch touching rescope-vendor.ts, which runs the full
rescope check: the knip-logger-console exact edit targeted the
packages/util/home knip section that #2758 deleted (drop the edit), the
zh vendoring-cookbook anchor predates the rescope.zh.md link
localization (follow it), and the new shipped-root.spec.ts joins the
files whose bare 'cordis' tokens are preset ids.
The files constraint tables gained per-package expectations on master
while this branch changed two files lists: apps/cli no longer ships
config/, and dsh-agent-presets ships presets/ (ordered where the
expected-files derivation places extras).
Review asked why the launcher special-cases one plugin's row. It no
longer does: the four shipped compositions move into the package
(presets/, in files), dsh-agent-presets resolves its own shipped root
and prepends it before configured roots (includeShippedRoot, default
true, opt-out for bare-machinery embedders), and the per-composition
derived patch, its spec, and the dump layer are deleted — profile-boot
and dump-config return to plain layer stacking. The always-load
guarantee now rides the schema default instead of patch ordering, so a
whole-config replacement keeps the shipped set and the squash, reload
freeze, and dump divergence stop being possible.
Gate globs, the web scaffold, and both preset browser lanes drop their
hand-fed shipped roots; the roster e2e keeps asserting configured roots
beside the shipped four against the built lib.
Fixes#2863.
The Windows native coverage lane was hitting Vitest's 30s per-test
ceiling on slow subprocess/ACP fixtures. Doubling the per-test budget
absorbs cold-start and process-teardown jitter without changing the
assertions or the job-level 120-minute cap.
python-release.yml only triggers on workflow_dispatch, so build.if:
github.event_name == 'workflow_dispatch' is always true and redundant; remove it
(the exact event set is already pinned in the spec). Update the spec assertion
accordingly.
Address PR #2875 review:
- Re-record python/development.i18n.yaml (corpus verify-translation-pairing was
out of sync after editing development.md/zh.md) and the 2026-08-11
python-publication-workflow pair after the dry-run wording tweak.
- Tighten the python-release spec assertion to the exact event set
(['workflow_dispatch']) instead of not.toHaveProperty('pull_request').
- Fix the 'dry-run run' wording in development.md and the note.
Corpus-wide verify-translation-pairing (1001 pairs) and note-format (594) pass;
ci-workflow.spec.ts 14/14.
The static gate (translation pairing) failed on a pre-existing master note:
2026-08-20-composer-edit-range-from-selection.zh.md:17 linked the zh target with
the en .md path. Point it at the .zh.md target and re-record the i18n hash. This
unblocks the required node 24 / static gate (it is not part of the python-release
gray-check change but sits on the same PR's CI path).
An intermediate cache iteration made cachedSnapshot async, which forced
listProjectionsFor onto a multi-line async signature; the final design
kept the sync read but the formatting residue stayed. No behavior
change.
The per-record contract test forced readFile to fail via chmod 0o000,
which is a no-op on win32, so the readRecord catch stayed uncovered on
the windows native coverage gate. Route record documents without an
isFile pre-filter: a directory where the document should be throws
EISDIR on every platform, and readRecord's existing contract already
reads an unreadable document as absent. Drop the win32 skip.
Review follow-ups: enumerate the derived shipped agent-preset root in
apps/cli README/reference dumps and the profile-boot module JSDoc
(bilingual pairs re-recorded), correct the stale AppCLIEntry/distIndex
analogy in the web scaffold and the shipped-root cross-reference in the
web preset e2e, drop the write-only ComposedProfile.rows field, and make
the Agent Note describe the dump path as sharing the derivation rather
than the builder.
Master's #2844 asserted composer recovery through a textarea locator the
Lexical composer no longer renders; the merge carried it in silently and
the CI web-snapshot lane timed out waiting for it. Use the same
data-composer-input wait the file's other cases already migrated to.
The master merge brought the doc-sync regenerations (locale-specific
paired document paths). Re-apply the zh-side link fixes to the agent
note and the session-projection intro, re-record translation pairing,
and keep the e2e event arrays as asserted literals like master.
The per-record rm in the cache spec hits an EPERM flake on windows
native (directory still draining); retry the recursive rm like the
subagent spec does. The unreadable-record probe is meaningless on
windows, where chmod 0o000 is a no-op; skip it there.
The reworked cache moved from the awaited coldSnapshot(id) to a sync
coldSnapshot(meta, events) whose write-back is fire-and-forget, and the
e2e fixture dropped the seeding call with it. The web bundle mounts the
cache again, so list rows read projection columns from stored rows only;
the one-shot and grandchild fixtures therefore lost their projections
column and the tree golden drifted. Re-seed both through the new API and
poll for the write-back to land.
Remove the pull_request:[labeled] trigger from python-release.yml so the
workflow no longer fires (and shows a gray skipped check) when a PR gets any
non-dry-run label. The credential-free dry-run validation is now manual-only
(workflow_dispatch with publish=false), preserving the validation capability
without a PR gray segment.
- python-release.yml: on is workflow_dispatch only; build.if is
github.event_name == 'workflow_dispatch'.
- ci-workflow.spec.ts: assert python-release has no pull_request event and the
simplified build.if.
- python/development.(md,zh.md) and 2026-08-11-python-publication-workflow note
(en/zh/i18n): describe the manual dispatch-only dry-run path.
Verification: ci-workflow.spec.ts 14/14, typecheck clean, note-format 585,
verify-translation-pairing consistent.
The boot-time agent-presets overlay replaced the composed roots with the
shipped root alone, so roots configured in a profile's cordis.patch.yml
vanished from the roster (externally reported in
deepseek-ai/deepseek-harness#3636). The overlay also froze the row's
boot-time config above every live reload and never reached the config
dump, which therefore showed roots the boot dropped.
Derive the roster patch from the current layers instead: prepend the
shipped root (system trust, wins duplicate ids) to configured roots,
share one builder across boot, live user-layer reloads, and
--dump-config, and fail loud on a roots value the launcher cannot
statically rewrite.
Fixes#2863.
isKeyboardSelectable() defaulted to true, so an arrow key at a chip edge
created a NodeSelection whose DOM projection collapses to an element
point; the plain-text binding's arrow/delete/insert handlers all bail on
non-Range selections, deadlocking arrows, typing, and Backspace at the
chip until a pointer click. False restores the placeholder semantics:
arrows cross the chip in one move and Backspace/Delete remove it whole.
Reproduced and verified with real-key Playwright probes (CDP raw
keydowns carry no engine default and cannot reproduce it); the
reference-composer e2e pins the gesture in the browser lane.
- drop the space-key debug probe from the production keymap
- give pastes their own undo boundary (PASTE_TAG via $addUpdateTag on the
nested dispatch path), with an input-bar regression test
- claim decoration outranks text-ref entities on the leading-token seat:
the entity transform skips the active claim token, restoring the warn
color for lexicon-listed command names (probe-confirmed regression test)
- caret-only commits no longer advance draftRev or re-publish InputState;
content changes still do (snapshot-built CAS spans stay valid)
- retire stale JSDoc/contract wording (paste-upgrade, set-invalid; the
invalid bit only promises the render treatment)
- rename the keydown probe spec to keymap-routing and drop test dead code
- reference-composer e2e gains the #2813 type-ahead-of-chip gesture
- archive three superseded composer notes (Safari soft-wrap, text layers,
decoration keys), rewrite the input-machine note's superseded half in
place, and record the new behavior decisions in the Lexical note
Every consecutive composer send waits out the submit round-trip's
read-only span; the end-of-document caret gesture becomes select-all +
ArrowRight (Cmd/Ctrl+End moves no caret in mac contenteditable), which
lets Lexical's own ancestor scroll walk prove the typing reveal.
textContent/data-placeholder probes replace inputValue/placeholder reads,
evaluate-string selectors move to the composer anchor, queued fills wait
out the submit round-trip's read-only span, and the refreshed aria goldens
drop the hover tooltip the old interaction order happened to capture.
Paste-command text entry (awaiting its microtask commit), data-placeholder
lookup, a scrollIntoView stub for the menu the settled-caret re-track now
opens, and a trailing separator on the bare /plan paste so Enter submits
instead of picking from that menu.
The client-bundle preset pins the production/development exports condition
(lexical's node-condition file selects its flavor with a top-level await a
CJS bundle cannot carry); the composer carries an explicit aria-label (a
div's data-placeholder does not name it the way a textarea placeholder
did); READMEs, the composer.bar slot doc, and the Agent Note record the
editor architecture; web e2e drives the contenteditable surface.
Drive keyboard gestures as real KeyboardEvents at the contenteditable
(Lexical routes them through the command layer), write drafts through the
shell, and probe decorations at their new DOM (chip decorators, styled
claim leaf, hint CSS variable, text-ref entity nodes). jsdom lacks
Selection.modify, so the Backspace-deletes-chip gesture moves to the
browser lane.
The editor (shell-owned, per-session) is the draft + chip truth; the
machine slims to the submit plane. Chips are atomic decorator nodes with
NodeKey identity; TokenSpan coordinates ride the detect projection (chip =
one U+FFFC), persistence and InputState.draft ride the clipboard
projection. The mirror/backdrop layers, Safari soft-wrap repair, manual
undo log, boundary occurrence deletion, and clipboard expansion all
retire; the producerless paste-attempt and set-invalid planes go with
them.
The two standalone unit classes deliberately mirror the KvUnit drain/guard
lifecycle (close + assertOpen); mark the block with a reason-carrying
jscpd ignore so the duplication gate stays green.
Regenerate config/cordis catalogs and doc graphs (the master merge changed
configs and consumers), add the DomainSpec layout field to the storage
type-equiv block, record session-projection-cache as a session/created
consumer, and drop the leftover experimental/team ghost directories from
the master rename. All 37 static gates pass.
Keep the base class's relative method order (write before coldSnapshot) so
the diff against the base shows the cold-read methods as a pure insertion
instead of a reorder of existing methods.
A session that never talks — a forked child seeded with its ancestor's
title, say — previously got its first cache row only at detach; a crash,
or a fork held live in the store, left the seed-derived values (the
title) unreadable on the cold list. Session creation is now a third
mandatory write point: the creation checkpoint folds the seed and
persists immediately. Write-policy docs (README + catalogs) updated.
Opening a per-record unit splits a legacy `<root>/<name>.json` (the
pre-per-record single-file layout) into per-record documents; an
already-present new record wins, and the legacy file is deleted once
every record migrated. The migration also runs when the new tree is
absent — the fresh-upgrade shape — and foreign, shapeless, or malformed
legacy files are left alone. This preserves previously cached session
titles, list metadata, stats, and subagent identity across the medium
change.
A detached history read still traverses the complete log, but each unit's
fold is now seeded from its cached checkpoint: the registry's restore
slices off the already-folded prefix (events at or below the row's seq)
and applies only the tail. The first cold read writes the refreshed
checkpoint back (fail-soft), so the cache row is created on first read
and kept current afterwards. The recipe lives on the cache
(cachedCheckpoint, coldSnapshot, writeBack); the api-proxy carrier only
supplies the stored header and the full log.
Cache README (EN/ZH): the medium is the session_projcache domain in
per-record layout (one version-stamped document per session under the
json backend root), reads are synchronous from the domain's in-memory
tables, and the storage stack rides in base. storage-json README
documents both layouts and their contracts; web-app README notes that
storage and the projection cache live in the shared base. Regenerated:
session-projection subsystem catalog (sync cachedSnapshot, domain
medium), config-catalog (Config.root gone), module-graph (cache now
depends on storage-domain, not session-persistence), cli composition
(storage rows in base), and the projection-cache Agent Note — which now
records the file-root revision and its revert as rejected alternatives.
cache.spec now boots the real storage stack (storage, storage-json,
storage-domain) and asserts the per-record medium directly:
<root>/session_projcache/sessions/<id>.json carries a version-stamped
{version, record} document, cachedSnapshot is synchronous (zero-I/O from
the domain's in-memory tables), and the write-policy / fail-soft / listing
coverage is preserved at 100%. json-backend.spec gains a per-record layout
block (per-record documents, overwrite/delete/reopen, unsafe keys and
undeclared tables rejecting, foreign-document discard on open, closed
guard, close drain, unreadable-as-absent); storage-domain domain.spec
covers layout validation and descriptorOf projection. list-children.spec
mounts the storage stack for its projectionCache cases and its
cachedSnapshot mocks and reads go synchronous; the api-proxy specs' cache
mocks go synchronous too. devDeps and tsconfig references updated for the
storage stack.
- document the exported SqliteStore prefix/suffix loaders (verify-export-jsdoc)
- share createStoredEventRead from session-persistence so the standalone
SQLite store stops duplicating the service helper (duplication gate)
- route replaceStored header upserts through writeRow (duplication gate)
- move replacement/conflict test SQL into closed test resources so the
SQLite SQL resource boundary test passes
The storage hub, json backend, and domain form are general infrastructure,
and the projection cache is a session-layer service that depends on them —
both belong in the shared base, not in the web overlay. Base now provides
storage / storage-json / storage-domain / session-projection-cache; the
web-app overlay keeps its surface consumers (workspace, message-feedback),
which inherit storageDomain from base (a child layer sees parent services).
This reverts the storage stack's historical web-app-only placement and the
file-root design's base mount of the cache.
The cache now opens its domain through ctx.storageDomain, which the
web-app overlay provides (storage-json + storage-domain, backend json).
A parent layer cannot see a child layer's services, so the base-layer
mount from the file-root design is reverted: the cache mount moves back
to web-app next to its storage dependencies, and Config.root is gone.
Restore the storage-domain medium the file-root design replaced: the cache
opens the session_projcache domain (per-record layout — one document per
session under the json backend root) and checkpoint writes land through
the domain's write chain. Reads and writes now share ONE coherent state:
cachedSnapshot reads synchronously from the domain's in-memory tables,
and every write is durability-first-then-memory, so a read can never go
around the write chain to the medium. The hand-rolled write chains,
in-flight tracking, per-session file paths, owner-only file modes, and
the sqlite no-path special case are gone; Config.root is removed and the
domain's version stamp makes a checkpointRecord bump discard stale
sessions per record instead of rejecting the whole medium. The async
ripple of the old file read is reverted: api-proxy's listing column and
subagent's cold identity read go back to synchronous cachedSnapshot.
The json backend now serves two layouts. single (the default) keeps the
whole unit as one document at <root>/<name>.json; per-record keeps one
version-stamped document per record at <root>/<name>/<table>/<key>.json
(plus global.json), so one write rewrites one record instead of the whole
unit. The per-record unit is stateless — the directory is the state,
loadAll re-reads the tree, and every write is a single durable file
operation — while single keeps its authoritative in-memory state and
whole-file publish. Records keys must be path-safe ([a-zA-Z0-9_-]+);
an unsafe key rejects. A record document that is malformed or stamped
with another version reads as an absent record: one bad or stale file
never bricks the unit, and a version bump discards stale records instead
of migrating them. DomainSpec and KvUnitDescriptor gain the optional
layout field (defineDomain validates it, descriptorOf projects it).
- Track fire-and-forget durable writes and await them at plugin disposal so
a late flush can never land after teardown (fixes the ENOTEMPTY cleanup
race in the disposal test).
- Drop the now-async-less Service.init and flushSoft void operators to keep
lint clean, and remove the redundant dsh-storage-json devDependency.
- Regenerate the stale pnpm lockfile and the config/persistence/module-graph
catalogs (with zh mirrors and pairing records) that the per-session cache
merge left out of sync, and fix the session-projection type-equiv doc
blocks to match the source.
- Add coverage for the unrelated-log-identity and no-per-session-directory
(sqlite) cold-read paths.
- subsystem docs: drop the removed persist flag from the ProjectionDefinition
type block (both languages).
- config-catalog regenerated (cache requires sessionProjections/sessions,
config gains root) and the zh side synced by hand; doc graphs regenerated.
- Agent Notes: the per-session cache note records the owned root tree and
no-persistence design; the storage-root proposal's link to it is corrected
(two levels up).
The web e2e seeds no longer warm the deleted coldSnapshot; assertions do not
depend on the cache path. list-children mounts the cache with a scratch
root instead of a storage-domain backend, and drops the now-unused storage
devDependencies.
The cache now owns its storage root (dshHomePath('projections')), so the
mount moves from the web-app overlay to base with that root declared, next
to the other base session layers. web-app inherits it; its overlay mount is
removed.
Store each session's projection_cache.json under the cache's own root tree
(<root>/<session-id>/projection_cache.json, wired to dshHomePath('projections')
in the base bundle) instead of beside the session log via
sessionPersistence.locate(). The cache owns its directory layout, keys
directories by the code-generated session id, and never consults the
persistence layer; the service now injects only sessionProjections and
sessions.
Drop the coldSnapshot method and its readFrom-tail fold ladder: every cold
consumer refolds from the log itself, so the cache only serves the listing
read (cachedSnapshot, one async file read per session) and the write side.
Fail-soft durability, per-path write serialization, in-flight drain, and
atomic 0600 writes are unchanged; the chain cleanup now observes its own
rejection so a failed write cannot surface as an unhandled error.
dsh-session-persistence leaves peer/dev dependencies and the tsconfig
reference; dsh-atomic-write moves to peerDependencies. Config gains a
required root.
- Write through @deepseek-ai/dsh-atomic-write with { mode: 0o600,
dirMode: 0o700 } instead of exporting a second atomic-write primitive
from dsh-storage-json; the session tree stays owner-only like the jsonl
backend's own directories.
- Serialize atomic replacements per cache path so an older cut can never
overwrite a newer one; track in-flight writes and drain them on
disposal so a late flush cannot land after teardown.
- Detect the absent per-session directory before the checkpoint cut and
durability flush: sqlite-style backends no-op the write entirely.
- Cold-read write-back path and identity both come from the stored log
header (tail.meta), so a stale caller header cannot mint an orphan
cache file.
- Add no-path coverage (write no-op, cachedSnapshot undefined, cold
fallback to the full-log rung) and a concurrent-write ordering test;
the package now holds 100% statement/branch/function/line coverage.
- Sync README.md/zh (inject list, coldSnapshot signature, per-session
file read wording), package description, the Agent Note alternatives,
and the superseded proposed/implemented notes (EN/ZH); add the
concurrent-checkpoint Known Limitation.
history and session.list baselines built through restore/snapshot without
wireOnly leaked host-only unit state onto the wire; pass { wireOnly: true }
on the detached history fold, the attached history snapshot, and the
attached listing snapshot.
Replace the single global session_projcache domain with a per-session
cache file inside the session's own persistence directory, resolved
through sessionPersistence.locate(meta) — the persistence backend owns
the session-directory layout, the cache service keeps every checkpoint
and cold-read responsibility.
- cachedSnapshot(meta) becomes async (one file read per session);
coldSnapshot takes the session header so it can locate the file, with
the stored log header remaining the identity witness.
- Backends without a per-session directory (sqlite) disable the durable
cache: writes no-op and cold reads fall to the full-log rung. An
obsolete global cache is never read — derived data refolds on first
cold read (no migration).
- writeAtomic is exported from dsh-storage-json as the shared atomic
whole-file replace primitive; api-proxy listing and subagent cold
reads await the now-async cachedSnapshot.
- READMEs and a new Agent Note document the per-session medium.
The product model has no cross-process writer exclusion (the coordinator
serializes per-session operations in-process; the README documents one
live writer per session), so the wx-created .lock sibling only guarded
byte-level races while adding two failure modes: a crash leaves a stale
lock that permanently wedges that log's appends/repairs/replacements, and
a post-commit lock cleanup failure makes a committed append look failed,
so the retained write-behind batch retries into duplicate seqs.
Remove withLogLock and keep replaceStored's revision compare-and-swap at
the commit boundary (recheck immediately before the atomic rename).
buildStepIndex rejected the whole decoder at initialization whenever any
registered step could not reach the current version, so one retired old
upgrader blocked every session, including later versions whose path to
the current version is complete. planSteps already refuses a specific
stored version when a needed step is missing; initialization now checks
only step legality and duplicates.
- user-approval: name the absent agent loop when the turnBoundary
projection is not registered (instead of the misleading 'outside an
open turn') and declare dsh-agent-loop as a peer dependency; add a
regression test for the missing-unit composition (v4p).
- session-title / time-context / agent-instructions: converge the
self-registered-key-absent handling to a loud throw with the same
v8-ignored comment style (v5 suggestion).
Update in place the implemented notes whose mechanisms this stack changed:
the subagent list identity note drops the deleted
SUBAGENT_CONTROL_PROJECTIONS_UNAVAILABLE error contract and hostile-unit
probe for the required-injection seam; the durable-subagent-catalog note
drops the same stale error-code reference; the sandbox pair replaces the
effectiveSandboxMode/effectiveApprovalPolicy trio with the sandboxMode
projection unit on the required registry; the plan collaboration note
replaces foldPlanMode with the plan projection unit. EN/ZH in lock-step;
i18n pairing re-recorded.
Per the imccyu review, the pre-existing goal, permissions, and plan units
go back to registering through the ctx.inject(['sessionProjections'], …)
child form instead of the required-inject direct register; goal returns to
zero diff (its service never reads projections). The mandatory-seam rework
for these three sites moves to a follow-up PR. New projection units keep
the required-inject direct-register form.
Regenerated catalog and subsystem docs follow the reverted service
signatures.
Reconcile the base's persist removal and later master content with the
migration branch: keep 2742's wireOnly read options, the host-inclusive
snapshot/viewCheckpoint/restore defaults, and the drive's late-event
replay (applyToCell) that bare-session reads rely on.
The note was committed on 2026-08-19 but filed under 2026-08-07 while
building on the 08-19 state-and-client-views note; rename the triplet to
align the filename with the actual date and update the inbound
architecture links.
Adapts this branch to the base's removal of the persist opt-in: the
registry folds and checkpoints every registered unit, the host-only
subagent identity drops its explicit persist: true, and the
state-and-client-views note records the uniform rule. The cordis API
catalog and session-projection subsystem signatures are regenerated.
The titleInput unit no longer retains the full eligible message history
in bounded reverse-linked chunks. It folds only {first, last, count} —
the values scheduling and fallback reads need — and the full eligible
prefix for one provider generation is scanned from the session log at
execution time. The projection state is O(1) per session instead of
growing with every user message.
The README description updates accordingly and drops the inaccurate
"latest request route" claim; the projection test now asserts the
bounded aggregate and its checkpoint row.
- llm-retry: validate config before registering the projection unit;
document the branded-retry-id zod cast; start stateVersion at 1.
- agent-loop: register turnBoundary only after every config validation,
so a rejected constructor leaves no unit behind; the defensive-cap
test no longer needs fiber cleanup.
- agent-instructions: keep newest-first per-scope change history so the
latest visible change survives a surface replacement shadowing the
newest one (restores the previous scan-visible semantics); add a
regression test for the delete-after-shadow sequence.
- tool-skill: keep catalog-message history so a shadowed newest catalog
message still falls back to the latest visible digest.
- session-query-sqlite: drop the unused required sessionProjections
injection.
- plan-mode: restore the command/done error-drop regression test and the
cold-replay command/done fold; drop the inaccurate state-reference
comment.
- tool-todo: remove a stray blank line; document the turnBoundary
reader contract on the projection type.
The snapshot embedded five live bilingual document pairs as reviewed
examples, so editing any of them (README, development guide, i18n docs)
churned the snapshot. Replace them with three synthetic fixture pairs
(product, rules, agent-note shapes) under scripts/fixtures; the prompt
examples stay representative without tracking real document content.
# Agent Note: GUI layering and the RPC protocol — host/client layering by capability provider, the four-quadrant message model, and the fetch carrier
Status: implemented
Archived: 2026-08-27
English | [中文](2026-07-19-gui-layering-and-rpc-protocol.zh.md)
@@ -209,7 +210,7 @@ The same domain tree as `ApiProxy`, but unary methods **take the business payloa
### The instance-level envelope observation aspect
All four quadrant full forms pass through `onEnvelope`; the base implementation is an **instance-owned microtask-batched buffer** (frame storms must not disturb consumers per frame; module-level state would leak across instances/tests, hence instance-owned). Observers subscribe via `subscribeEnvelopes(listener)` (receiving whole batches as `readonly RpcMessage[]`, returning an unsubscribe function); a listener throw is isolated (observation must never bite the carrier). With no subscribers the buffering costs nothing. No shipped consumer subscribes today — the aspect is the designated seat for wire diagnostics (the retired RPC debug panel was its first consumer, and a future one plugs in without touching the carrier).
All four quadrant full forms pass through `onEnvelope`; the base implementation is an **instance-owned microtask-batched buffer** (frame storms must not disturb consumers per frame; module-level state would leak across instances/tests, hence instance-owned). Observers subscribe via `subscribeEnvelopes(listener)` (receiving whole batches as `readonly RpcMessage[]`, returning an unsubscribe function); a listener throw is isolated (observation must never bite the carrier). With no subscribers the buffering costs nothing. No shipped consumer subscribes — the aspect is the designated seat for wire diagnostics (the retired RPC debug panel was its first consumer, and a future one plugs in without touching the carrier).
# Agent Note: WebSocket carrier for browser downlinks
Status: implemented
Archived: 2026-08-27
English | [中文](2026-08-04-websocket-downlink-carrier.zh.md)
@@ -16,7 +17,7 @@ WebSocket carries only the host→browser downlink. All client→host unary call
## Upgrade and lifecycle boundaries
`dsh-host-webserver` provides an exact upgrade-route registration point alongside ordinary routes, dispatches Node upgrade sockets by pathname only, contains raw-socket errors, and waits for surviving upgraded connections to close during server teardown; it knows nothing about Harness frames or WebSocket messages. `dsh-client-connection` owns the WebSocket handshake, frame output, and stream cancellation, and reuses the `/api` Host/Origin trust fence before upgrade. An untrusted authority or cross-origin Origin is rejected before `ctx.apiProxy.events.*` starts.
`dsh-host-webserver` provides an exact upgrade-route registration point alongside ordinary routes, dispatches Node upgrade sockets by pathname only, contains raw-socket errors, and waits for surviving upgraded connections to close during server teardown; it knows nothing about Harness frames or WebSocket messages. `dsh-client-connection` owns the WebSocket handshake, frame output, and stream cancellation. Before upgrade it applies the `/api` Host/Origin checks followed by the same signed browser-cookie authentication as unary HTTP. An untrusted authority or cross-origin Origin receives 403; a trusted but unauthenticated request receives 401; neither starts a Remote stream.
A browser abort or socket close cancels the corresponding host stream; plugin teardown also waits for that source iterator's cleanup. If a host stream throws midway, the carrier sends one existing `stream/error` frame and then closes the socket; the client treats that frame as connection loss rather than delivering it to a business sink. Each WebSocket reports open independently, and the existing readiness handshake still waits until mux and host are both open and the `host.describe` HTTP call has succeeded before publishing connected.
English | [中文](2026-08-18-sqlite-physical-chunk-row-compression.zh.md)
@@ -12,15 +13,15 @@ A physical row that represents several events affects append contiguity, crash r
## Decision
`@deepseek-ai/dsh-session-persistence-sqlite` uses the packed schema-17 implementation. It is the only SQLite persistence package and provider; the predecessor scalar layout and the temporary versioned sibling are not retained. SQLite remains an opt-in switch, while shipped default compositions continue to use JSONL. Both backends implement the same `SessionPersistence` service through `PersistenceCoordinator`, so physical packing changes neither live event delivery nor the logical session API.
`@deepseek-ai/dsh-session-persistence-sqlite` uses the packed schema-20 implementation. It is the only SQLite persistence package and provider; the predecessor scalar layout and the temporary versioned sibling are not retained. SQLite remains an opt-in switch, while shipped default compositions continue to use JSONL. Both backends implement the same `SessionPersistence` service through `PersistenceCoordinator`, so physical packing changes neither live event delivery nor the logical session API.
Schema 17 keeps ordinary ROWID tables and the composite `events(session_id, seq)` primary-key index. Scalar rows represent one logical event. Packed rows use the storage tags `text-chunks`, `reasoning-chunks`, and `tool-call-chunks`; the SQL `seq` and `time` columns hold the first logical member, and `data` holds the packed payload. Packed rows set `ignorable=0` as a physical discriminator and leave `source_event_seqs` and `surface_op` as `NULL`; scalar rows use `ignorable=1` only for logical ignorable events and `NULL` otherwise. A future ignorable logical event may therefore reuse a storage-tag name without being decoded as a packed row. The tags are storage vocabulary, not `SessionEventMap` members.
Schema 20 keeps ordinary ROWID tables and the composite `events(session_id, seq)` primary-key index. Scalar rows represent one logical event. Packed rows use the storage tags `text-chunks`, `reasoning-chunks`, and `tool-call-chunks`; the SQL `seq` and `time` columns hold the first logical member, and `data` holds the packed payload. Packed rows set `ignorable=0` as a physical discriminator and leave `source_event_seqs` and `surface_op` as `NULL`; scalar rows use `ignorable=1` only for logical ignorable events and `NULL` otherwise. A future ignorable logical event may therefore reuse a storage-tag name without being decoded as a packed row. The tags are storage vocabulary, not `SessionEventMap` members.
SQLite owns chunk encoding and validation inside the schema-17 package. Exact-field whitelisting means unknown fields, surface metadata, incompatible chunk identity, sequence gaps, and unsafe timestamps remain scalar rather than losing information. One packed row represents at most 1,024 events and 1 MiB of uncompressed UTF-8 `data`; the encoder partitions longer runs, and the decoder rejects rows outside those format limits.
SQLite owns chunk encoding and validation inside the schema-20 package. Exact-field whitelisting means unknown fields, surface metadata, incompatible chunk identity, sequence gaps, and unsafe timestamps remain scalar rather than losing information. One packed row represents at most 1,024 events and 1 MiB of uncompressed UTF-8 `data`; the encoder partitions longer runs, and the decoder rejects rows outside those format limits.
The `data` column accepts `TEXT` or `BLOB`. Serialized values below 4 KiB remain text. At or above the threshold, the writer uses Zstandard level 3 and retains the frame only when it is smaller than the text; the reader decompresses the blob before strict UTF-8 decoding and JSON parsing. The fixed moderate level and threshold limit frame overhead and synchronous CPU work while capturing the repeated payloads that dominate retained bytes.
`source_event_seqs` remains the complete ordered list of earlier events cited by a surface node, including every streamed chunk behind an assembled assistant message. Schema 17 stores the first sequence as an unsigned varint and every subsequent signed difference as a ZigZag varint. This preserves arbitrary order and every sequence while exploiting the overwhelmingly consecutive lists produced by streaming. An empty list is an empty non-null blob, distinct from absent provenance.
`source_event_seqs` remains the complete ordered list of earlier events cited by a surface node, including every streamed chunk behind an assembled assistant message. Schema 20 stores the first sequence as an unsigned varint and every subsequent signed difference as a ZigZag varint. This preserves arbitrary order and every sequence while exploiting the overwhelmingly consecutive lists produced by streaming. An empty list is an empty non-null blob, distinct from absent provenance.
### Transactional append packing
@@ -32,11 +33,11 @@ Normal append never deletes or replaces an earlier event row. Fixed write-behind
Full reads decode each physical row as one all-or-nothing logical span and validate contiguous logical sequences. A reverse pass identifies the last valid `turn/end` without retaining a second decoded copy of the full physical scan; the forward pass decodes one row at a time into the required logical result. A malformed row or gap before that committed boundary is corruption; a malformed final physical row becomes the opaque repair marker at that row's base sequence. Recovery re-reads and validates that marker while holding the write lock, then deletes the whole physical row and any later rows before binding synthetic closers as scalar events. A stale repair cannot delete a newer writer's valid suffix.
`readFrom(id, fromSeq)` examines packed predecessors only within the maximum schema-17 row span, then reads from the earliest candidate that may contain `fromSeq`. The decoder filters reconstructed members below `fromSeq`, so a suffix may begin inside a packed row without parsing an unrelated earlier scalar row. Reading from that candidate also exposes an overlapping scalar row to contiguity validation instead of letting it hide the packed member. Packed data exceeding the uncompressed format byte limit rejects before JSON parsing.
`readFrom(id, fromSeq)` examines packed predecessors only within the maximum schema-20 row span, then reads from the earliest candidate that may contain `fromSeq`. The decoder filters reconstructed members below `fromSeq`, so a suffix may begin inside a packed row without parsing an unrelated earlier scalar row. Reading from that candidate also exposes an overlapping scalar row to contiguity validation instead of letting it hide the packed member. Packed data exceeding the uncompressed format byte limit rejects before JSON parsing.
### Schema ownership
A pristine database initializes at schema 17. Older physical schemas, foreign application identities, non-pristine unversioned databases, and incompatible schema objects reject; the pre-release package supplies no migration. Every connection disables trusted schemas and memory-mapped I/O before inspecting durable schema, then reads both settings back. After selecting and verifying the journal mode, the provider pins `synchronous=FULL` and verifies it so SQLite build defaults cannot weaken committed-append durability. Package code loads every statement and fixed pragma from closed-name `.sql` resources and binds runtime values as parameters.
A pristine database initializes at schema 20. Older physical schemas, foreign application identities, non-pristine unversioned databases, and incompatible schema objects reject; the pre-release package supplies no migration. Every connection disables trusted schemas and memory-mapped I/O before inspecting durable schema, then reads both settings back. After selecting and verifying the journal mode, the provider pins `synchronous=FULL` and verifies it so SQLite build defaults cannot weaken committed-append durability. Package code loads every statement and fixed pragma from closed-name `.sql` resources and binds runtime values as parameters.
### Physical-write regression
@@ -58,11 +59,11 @@ The repository regression guard writes 1,000 streamed deltas in 40-event durable
**Compress every payload.** Rejected because small independent Zstandard frames add headers and synchronous CPU work while losing the cross-record dictionary opportunity of a whole-file stream. On the 105-session comparison corpus, a threshold sweep produced 75.01 MB at 4 KiB, versus 93.87 MB at 16 KiB and 60.92 MB at 1 KiB. The writer fixes level 3 rather than inheriting a library default, matching the moderate level used by [Codex cold-rollout compression](https://github.com/openai/codex/blob/main/codex-rs/rollout/src/compression.rs) while retaining independent row access.
The final frozen comparison used 105 sessions, 2,507,860 logical events, 512-event durable batches, three independent builds per backend, and three read passes per build. SQLite used 75.01 MB, wrote in 8.58 s, read complete sessions at 3.95/21.58 ms p50/p95, read 50-event tails at 0.253/0.378 ms, and forked every session in 13.10 s. Zstandard JSONL used 30.65 MB and measured 28.21 s, 4.49/23.36 ms, 10.58/80.90 ms, and 14.48 s. The predecessor scalar SQLite layout used 709.57 MB and measured 10.64 s, 9.02/69.16 ms, 0.189/0.293 ms, and 19.30 s. The packed layout is 89.4% smaller than the predecessor, writes 19.4% faster, improves complete-read p50/p95 by 56.2%/68.8%, and reduces 2,507,860 physical event rows to 65,810. Scalar tail-50 and list micro-latency are lower, but the packed provider remains materially faster than JSONL on those paths and wins the dominant size, write, full-read, and fork costs. The 4 KiB threshold is the accepted balance rather than a strict dominance claim.
The final frozen comparison used 105 sessions, 2,507,860 logical events, 512-event durable batches, three independent builds per backend, and three read passes per build. SQLite used 75.01 MB, wrote in 8.58 s, read complete sessions at 3.95/21.58 ms p50/p95, read 50-event tails at 0.253/0.378 ms, and forked every session in 13.10 s. Zstandard JSONL used 30.65 MB and measured 28.21 s, 4.49/23.36 ms, 10.58/80.90 ms, and 14.48 s. The predecessor scalar SQLite layout used 709.57 MB and measured 10.64 s, 9.02/69.16 ms, 0.189/0.293 ms, and 19.30 s. The packed layout is 89.4% smaller than the predecessor, writes 19.4% faster, improves complete-read p50/p95 by 56.2%/68.8%, and reduces 2,507,860 physical event rows to 65,810. Scalar tail-50 and list micro-latency are lower, but the packed provider remains materially faster than JSONL on those paths and wins the dominant size, write, full-read, and fork costs. The 4 KiB threshold is the accepted balance rather than a strict dominance claim. This comparison measured schema 17; its exact values are evidence for the original packed-row decision, not schema-20 measurements. The [persistence latency and page-size decision](2026-08-25-persistence-latency-and-page-size.md) owns the schema-19 benchmark and current encoding refinements.
**Store packed payloads under the logical `assistant/chunk` type.** Rejected because payload heuristics make malformed rows ambiguous and couple physical decoding to future logical payload fields. Explicit tags fail loudly.
**Store `SessionHeader` fields in an extensible metadata blob.** Rejected for schema 17 because `agentPreset` is a typed core resume invariant shared by JSONL and SQLite, not provider extension metadata. Persisting validated core fields directly keeps both backends aligned; an untyped catch-all would add another compatibility mechanism without a current producer. Revisit this only with a core-owned, namespaced `SessionHeader` extension protocol implemented by every backend.
**Store `SessionHeader` fields in an extensible metadata blob.** Rejected for schema 20 because `agentPreset` is a typed core resume invariant shared by JSONL and SQLite, not provider extension metadata. Persisting validated core fields directly keeps both backends aligned; an untyped catch-all would add another compatibility mechanism without a current producer. Revisit this only with a core-owned, namespaced `SessionHeader` extension protocol implemented by every backend.
**Expose compression rules through configuration or a live registry.** Rejected because same-version databases must be readable independently of runtime topology. The codec is modular source code, but the durable rule set is fixed by schema version.
# Agent Note: Equal-order system-prompt sections render in activation order
Status: implemented
Archived: 2026-08-25
English | [中文](2026-08-24-system-prompt-section-order-ties.zh.md)
## Problem
`SystemPromptRegistry` sorts sections by `order` with a stable sort, so equal orders render in plugin-activation order. `tool:cordis` and `tool:workflow` both declared `order: 115`, while their activation order varies between clean platform compositions. ACP and SDK snapshot replays could therefore assemble the same sections in a different order from their committed `system-prompt.expected.md` files.
## Decision
Give the affected sequence distinct values without changing its established relative order: `tool:cordis` stays at 115, `tool:workflow` uses 115.5, `tool:ralph` stays at 116, continuable subagent guidance stays at 116.5, and child-report guidance stays at 117. Prompt text and tool schemas remain unchanged.
## Alternatives considered
**Normalize section order in the snapshot harness.** Rejected because the runtime, request header, and model prompt would remain sensitive to activation timing while only the fixture comparison hid the difference.
**Tie-break equal orders by section name in the registry.** Rejected because it would silently reorder every existing tie. Explicit orders keep each model-visible placement local to the contributing plugin.
## Consequences
The Cordis and workflow guidance has a platform-independent order while Ralph remains before continuable subagent and child-report guidance. Prompt-section placements that require a stable relative position need distinct `order` values; other equal-order sections retain activation-order semantics and are outside this decision.
## Testing
The keyless ACP and SDK snapshot replays pin Cordis before workflow and preserve the workflow, Ralph, continuable-subagent, and child-report sequence. The full snapshot suite verifies the refreshed fixtures.
English | [中文](2026-08-18-product-subagent-failure-facts.zh.md)
## Problem
The [Claude Code and Codex product providers](2026-08-04-claude-code-and-codex-subagent-backends.md) receive structured product failures, but a published run historically flattened most of them to the shared `error` stop reason. Product logs retained detail that the foreground parent and a [one-shot background Job](2026-08-12-product-subagent-one-shot-background-tasks.md) could not use to distinguish a product limit, an execution failure, or an early process exit.
Copying SDK error text, app-server payloads, or stderr into the result would expose task text, paths, environment values, credentials, or product internals. Adding shared error fields would also make the provider-neutral [subagent seam](2026-06-21-subagent-capability-seam.md) own product version vocabularies that change independently.
## Decision
Each product Provider owns the mapping from its pinned official structured failures, current operation, and managed process outcome to one fixed safe diagnostic line. `SubagentResult` remains unchanged: consumers receive the existing bounded `diagnostic` string and do not parse its product-private fields. The [minimal-diagnostics decision](../simplification/2026-08-21-product-subagent-minimal-diagnostics.md) supersedes this note's complete Claude Code subtype mirror; this note continues to own the current detailed Codex categories until that provider adopts the same simplification.
The Provider omits unavailable optional fields. Exit code and signal are independent facts and are each retained when observed. A contributing permission decision from the [non-interactive permissions decision](2026-08-15-product-subagent-noninteractive-permissions.md) follows the structured line; the latest safe permission fact remains operation-local. The shared result boundary limits the complete text to 4096 UTF-8 bytes.
Successful results and local cancellation expose no failure fact. Raw product errors, stderr, tool input, paths, environment values, credentials, and protocol payloads never enter the diagnostic. Startup and cleanup rejections use the same safe line in their Error message. Original failures remain on internal cause chains; Provider Host logs and forwarded stderr remain product-local observation only.
### Claude Code facts
The [minimal-diagnostics decision](../simplification/2026-08-21-product-subagent-minimal-diagnostics.md) exclusively owns Claude Code categories, stages, process facts, permission ordering, and verification for Agent SDK 0.3.241 and Claude Code 2.1.241. This note carries no separate Claude category contract.
### Codex facts
Codex app-server 0.147.0 defines eleven string categories and five object variants. The Provider preserves `contextWindowExceeded`, `sessionBudgetExceeded`, `usageLimitExceeded`, `serverOverloaded`, `cyberPolicy`, `internalServerError`, `unauthorized`, `badRequest`, `threadRollbackFailed`, `sandboxError`, and `other`. It also preserves `httpConnectionFailed`, `responseStreamConnectionFailed`, `responseStreamDisconnected`, `responseTooManyFailedAttempts`, and `activeTurnNotSteerable`; the four connection/stream variants retain numeric `httpStatusCode`, while the active-turn variant does not expose `turnKind`. Unknown strings, objects with another variant set, malformed values, and unclassified exceptions use `unknown`.
| Stage | Owned operation | Observable failure |
| --- | --- | --- |
| `initialize` | App-server spawn and initialize/initialized handshake | `start()` rejects with fixed safe facts and any process outcome already observed |
| `thread-start` | Ephemeral `thread/start` request and response validation | `start()` rejects with the thread stage and any available process outcome |
| `turn-start` | Published `turn/start` request, provisional ids, and early frames | The run resolves as `error` with a safe unknown fallback when no structured category exists |
| `turn` | Terminal notification, final-answer selection, and error-info mapping | The complete category and optional HTTP status reach the non-completed result |
| `process` | Managed app-server exits before another terminal path settles | The run resolves as `error` with `process-exit` and any available code and signal |
| `teardown` | Wire close and process-tree release | `dispose()` rejects independently; startup rollback aggregation exposes both startup and teardown lines |
`contextWindowExceeded` remains `max-tokens`; every other known or unknown Codex category remains `error`, and `cyberPolicy` does not become `refusal`.
### Ownership and lifecycle
| Fact or resource | Owner | Consumer behavior |
| --- | --- | --- |
| Codex error category | Codex Provider over its pinned official app-server | The Provider preserves its current structured category and uses `unknown` outside the recognized set |
| Current failure stage | Product Provider operation | Derived at the failure site; never persisted or used as a recovery state |
| Exit code and signal | `dsh-subprocess` process handle | The Provider displays observed values without inferring missing ones |
| Diagnostic bytes and delivery | `dsh-subagent`, foreground tool, and Job runtime | The same bounded text is presented separately from assistant output in both scheduling modes |
| Raw product failure | Product runtime, internal cause chain, and Host observation | It remains internal and never becomes model-visible result text |
## Verification
Claude Code verification is owned by the [minimal-diagnostics decision](../simplification/2026-08-21-product-subagent-minimal-diagnostics.md). Codex package tests pin all sixteen current error-info variants, HTTP status presence and absence, all six stages, unknown fallback, stop-reason preservation, permission ordering, sanitization, cancellation, concurrency, and cleanup aggregation. The real app-server fixture produces an actual Codex `internalServerError` and covers process/protocol failure and whole-tree quiescence. The keyless ACP snapshot records the Codex diagnostic in foreground error output, a background completion notice, and `job_output`.
## Alternatives considered
**Return raw SDK errors, app-server payloads, or stderr.** These values can contain commands, paths, workspace content, environment values, credentials, or upstream prose. A fixed allowlisted mapping preserves actionable facts without expanding the model-visible trust boundary.
**Add a shared product-error enum or structured result fields.** Claude Code and Codex version their error unions independently. A shared enum would duplicate those authorities and force unrelated Providers and consumers to track product releases.
**Parse generic stderr and exception messages.** Free-form text is neither stable nor safe. Only pinned structured product fields and the managed process outcome qualify as diagnostic input.
**Persist stages or add a recovery controller.** The stage is derived from the current call site only when a failure is reported. Persistence, retries, resume, and remediation need separate ownership and user contracts.
**Map product limits to new shared stop reasons.** Claude Code turn and budget limits are not token-window exhaustion, and an error category does not establish refusal semantics. Existing stop reasons remain unchanged.
## Consequences
The parent can distinguish the current Codex budget, usage, service, policy, request, connection, stream, rollback, sandbox, and active-turn categories without receiving raw product text. The [minimal-diagnostics decision](../simplification/2026-08-21-product-subagent-minimal-diagnostics.md) owns the corresponding Claude result. Foreground and background scheduling preserve the same fact because both consume one `SubagentResult`.
The diagnostic is display text rather than a new public protocol. Callers may present it but must not branch on its punctuation or product-private category names. A pinned product-version upgrade revalidates the Provider mapping and evidence without requiring every official error member to remain model-visible.
This decision adds no product session persistence, retry policy, recovery state, stderr classifier, authentication or configuration taxonomy, progress stream, or human interaction path.
[最小诊断决策](../simplification/2026-08-21-product-subagent-minimal-diagnostics.zh.md)独占负责 Agent SDK 0.3.241 与 Claude Code 2.1.241 的 Claude Code 类别、阶段、进程事实、权限顺序与验证。本说明不再承载独立的 Claude 类别约定。
- Multimodal blocks return only with coordinated adapter, UI, and compaction support; see [the drop-image Agent Note](../../archived/simplification/2026-07-04-drop-image-content-block.md).
- Cache hints and assistant prefill remain absent until a shipping adapter can honor them; see the [producer-less variants](../../archived/simplification/2026-07-04-prune-producerless-vocabulary-variants.md) and [inert request knobs](../../archived/simplification/2026-07-04-drop-inert-request-knobs.md) Agent Notes.
- Every adapter pays a translation cost; the first real adapters have since validated the streaming protocol, and new adapters should continue proving their provider-specific mapping in adapter-local tests.
- IDs that cross package boundaries are branded (`CallId`, the shared agent/session `SessionId`) — nominal typing at zero runtime cost.
- IDs that cross package boundaries are branded (`ToolCallId`, the shared agent/session `SessionId`) — nominal typing at zero runtime cost.
@@ -6,7 +6,7 @@ English | [中文](2026-06-13-capability-seams.zh.md)
## Problem
The harness has swappable capabilities — bash execution today, sandboxed/remote executors and alternative model providers tomorrow. A capability has three concerns that change at different rates and for different reasons: the *contract* (what the capability is), the *implementation* (how it runs), and the *consumer API* (what the model and other plugins program against). Bundling them in one package couples those rates of change — swapping a local executor for a sandboxed one would churn the tool schemas the model sees, even though the model-facing contract never changed.
The harness has swappable capabilities, including shell execution and model providers. A capability has three concerns that change at different rates and for different reasons: the *contract* (what the capability is), the *implementation* (how it runs), and the *consumer API* (what the model and other plugins program against). Bundling them in one package couples those rates of change — swapping a local executor for a sandboxed one would churn the tool schemas the model sees, even though the model-facing contract never changed.
This is distinct from "who provides vs. needs a capability at runtime", which Cordis already answers with services + `inject` (a provider registers `ctx.shell`; a consumer declares `inject: ['bash']` and its fiber pends until the service exists). That mechanism is necessary but doesn't dictate package boundaries; this Agent Note does.
- **The canonical durable log persists every `SessionEvent` losslessly, including `assistant/chunk`.** JSONL storage may encode a consecutive delta run as one packed row, but logical readers reconstruct the exact event boundaries, sequence numbers, and timestamps. `deriveMessages()` skips chunks, and a chunk-filtered rollout (Codex's `policy.rs`) is tempting — but `seq = log.length` and validation of `events[i].seq === i` require a *contiguous* logical log; filtering chunks out would leave holes and break both the contract and resume. A chunk-filtered projection is possible later as a derived view with its own renumbering, but it is NOT the canonical log.
- **Append-only; a crashed turn is closed, never truncated.** Flushed events are never rewritten. The [semantic checkpoint policy](../bug-fix/2026-07-21-semantic-session-checkpoints.md) drains the request before model dispatch, a recorded top-level call before tool dispatch, and the complete response/result batch after a step; the loop drains the final turn boundary. Because one interrupted turn may contain substantial valid work, cold inspection preserves its contiguous, parseable events and adds risk-classified error results for unanswered assistant calls, a missing `step/end`, and `turn/end` with `{ kind: 'interrupted' }` to the in-memory logical view. `prepare` or `load` commits those closers before returning a recoverable view; the synthetic results keep resumed provider transcripts valid. Only an incomplete final record is discarded during committed repair; a parse error or sequence gap at or before the last real `turn/end` is corruption and makes the session unloadable.
- **File backend canonical, DB backend a proven drop-in.** `SessionEvent` maps 1:1 onto a row `(session_id, seq, type, time, data)` — `append` is INSERT (in a transaction asserting the contiguous-seq contract), and reads use SELECT … ORDER BY seq. `dsh-session-persistence-sqlite` is exactly this: a `SessionPersistence` subclass with no interface change (opencode runs this exact shape on SQLite/WAL), and it passes the same `runPersistenceContract` suite as the JSONL backend — so the contract holds both backends to identical semantics (lazy materialization, logical interrupted-turn closure, single committed repair, contiguous-seq), expressed once over file bytes and once over rows. Its database carries a dedicated application id and monotonic schema version. A pristine file creates all tables and stamps both header values in one transaction; an unversioned file with any user-defined schema object or application identity, a foreign current-version identity, and every non-current version reject before journal-mode mutation.
- **Metadata is out-of-log.** Format version, cwd, and lineage are storage concerns, not replayable conversation state, so they live in a `SessionHeader` owned by `dsh-session` and attached to a `Session` via a new readonly `session.header` — never in `SessionEventMap`, never reaching `deriveMessages()`. `createdAt` is non-negative safe-integer Unix epoch milliseconds: live creation and persistence registration reject fractional values, JSONL validates the decoded header, and SQLite stores it in a strict `INTEGER` column. The alternative (a merge-extensible `session/meta` event as log line 0) was rejected: an in-log event would ride along with a seeded/forked session for free, but metadata is not replayable state, so the explicit out-of-log header boundary is the cleaner cost. (The header was originally split into an immutable `SessionHeader` plus a mutable `SessionSummary` whose union was `SessionMeta`; the mutable summary was later removed as dead state — see [Drop the mutable session summary](../simplification/2026-06-19-drop-mutable-session-summary.md).)
- **The file backend is canonical while the service remains extensible.** `dsh-session-persistence-jsonl` is the sole first-party provider and passes `runPersistenceContract`; the abstract service and coordinator remain available to out-of-tree providers. The [JSONL-only persistence decision](../simplification/2026-08-30-jsonl-only-session-persistence.md) owns removal of the first-party database provider and its deliberate compatibility cut.
- **Metadata is out-of-log.** Format version, cwd, and lineage are storage concerns, not replayable conversation state, so they live in a `SessionHeader` owned by `dsh-session` and attached to a `Session` via a new readonly `session.header` — never in `SessionEventMap`, never reaching `deriveMessages()`. `createdAt` is non-negative safe-integer Unix epoch milliseconds: live creation and persistence registration reject fractional values, and JSONL validates the decoded header. The alternative (a merge-extensible `session/meta` event as log line 0) was rejected: an in-log event would ride along with a seeded/forked session for free, but metadata is not replayable state, so the explicit out-of-log header boundary is the cleaner cost. (The header was originally split into an immutable `SessionHeader` plus a mutable `SessionSummary` whose union was `SessionMeta`; the mutable summary was later removed as dead state — see [Drop the mutable session summary](../simplification/2026-06-19-drop-mutable-session-summary.md).)
- **`ctx.agents.create()` and `ctx.agents.resume()` are async factories; resume additionally crosses the persistence boundary.** `ctx.agents.resume({ resumeSessionId })` obtains the exact unpublished Session through `ctx.sessionPersistence.prepare()`, publishes it under the persisted id, and continues its projections. The [Session preparation decision](2026-08-05-session-preparation.md) owns reuse between history inspection and resume. The agent-loop does NOT hard-inject `sessionPersistence` (that would pend non-persistent demos forever); `resume` rejects with a clear error when it is absent.
## Alternatives considered
Each key choice above records its rejected alternative where the choice is stated: a **chunk-filtered canonical log** (Codex's `policy.rs` shape) — breaks the contiguous-seq contract; **truncating a crashed turn** — silently destroys a long autonomous run's real work; an **in-log `session/meta` event as line 0** — metadata is not replayable state; **finite fractional `createdAt` values** — have no producer and diverge from integer Unix-millisecond storage and query columns; **adopting a non-pristine unversioned SQLite file** — can overwrite unrelated objects or identity; **hard-injecting `sessionPersistence` into the loop** — would pend non-persistent demos forever.
Each key choice above records its rejected alternative where the choice is stated: a **chunk-filtered canonical log** (Codex's `policy.rs` shape) — breaks the contiguous-seq contract; **truncating a crashed turn** — silently destroys a long autonomous run's real work; an **in-log `session/meta` event as log line 0** — metadata is not replayable state; **finite fractional `createdAt` values** — have no producer and diverge from integer Unix-millisecond storage; **hard-injecting `sessionPersistence` into the loop** — would pend non-persistent demos forever.
Format versioning: the header carries a `version`; cold reads reject any non-current version. The pre-release session format stays pinned at `SESSION_FORMAT_VERSION = 0` and carries no broad compatibility promise, while the coordinator may own an explicit narrow import upgrade when persisted user data requires it ([pre-identity message recovery](../bug-fix/2026-07-28-load-pre-identity-session-messages.md)). Append-only + flush is robust to partial trailing writes (tolerated during cold preparation) but not to fsync-less powerloss mid-line; a DB/WAL backend is the stronger option there.
Format versioning: the header carries a `version`; cold reads reject any non-current version. The pre-release session format stays pinned at `SESSION_FORMAT_VERSION = 0` and carries no broad compatibility promise, while the coordinator may own an explicit narrow import upgrade when persisted user data requires it ([pre-identity message recovery](../bug-fix/2026-07-28-load-pre-identity-session-messages.md)). Append-only + flush is robust to partial trailing writes tolerated during cold preparation; a future provider or write-ahead log needs its own power-loss and recovery contract.
## Consequences
Two new packages and the metadata contract in `dsh-session` (`session.header`, the `create(id?, options?)` signature). Bought: durable resume/fork, a read/replay path, crash tolerance, and host-side session access over the existing event-sourced log, with the backend swappable behind one interface. The reusable `runPersistenceContract` suite holds every backend to the same append-only, contiguous-seq, lazy-materialization, logical-recovery, integer-metadata, and serializability semantics. Persisting the full logical log also settles event fidelity: every `assistant/chunk` survives exactly even when JSONL packs several into one storage row. SQLite initialization either commits its complete owned schema and header identity or leaves no partial schema to strand on the next open.
The Service Definition, JSONL provider, and metadata contract in `dsh-session` (`session.header`, the `create(id?, options?)` signature) buy durable resume/fork, a read/replay path, crash tolerance, and host-side session access over the existing event-sourced log. The reusable `runPersistenceContract` suite holds the provider and future implementations to the same append-only, contiguous-seq, lazy-materialization, logical-recovery, integer-metadata, and serializability semantics. Persisting the full logical log also settles event fidelity: every `assistant/chunk` survives exactly even when JSONL packs several into one storage row.
@@ -41,7 +41,7 @@ Delta processing is O(1) when no new events and O(new events) when new events ar
### Persistence
The new fields are serialized as top-level JSON properties. The JSONL backend requires zero changes — `JSON.stringify`/`JSON.parse` preserve everything transparently. The SQLite backend's `events` table carries two nullable TEXT columns (`source_event_seqs`, `surface_op`). The on-disk `SCHEMA_VERSION` is bumped to reflect the column set, and — per the pre-release bump-and-reject policy — a database written by any other build is REJECTED on open rather than migrated (there is no persisted user data to upgrade). The session format `version` is pinned at `SESSION_FORMAT_VERSION = 0` (the "unstable / pre-release" stance): the optional surface fields are absorbed without bumping it.
The new fields are serialized as top-level JSON properties. JSONL storage requires no separate column mapping: its lossless JSON boundary preserves both values. The session format `version` is pinned at `SESSION_FORMAT_VERSION = 0`; the optional surface fields are absorbed without bumping it.
### Crash recovery
@@ -64,7 +64,6 @@ Every surface-eligible event must carry `surfaceOp` or it would disappear from d
- **`packages/core/session`**: `surface.ts` (`SurfaceManager`) maintains one ordered seq array for candidate acceptance and live projection; `SessionSurface` is its readonly public view. `SurfaceOp`/`SurfaceIntent` and the top-level session-event fields record how entries join it. `append()` requires a `SurfaceIntent` for surface events, `deriveMessages()` walks the surface as the sole derivation path, and `repair.ts` emits surface-aware closers. The seed constructor rejects a surface-eligible seed event missing its `surfaceOp` marker (see § Invariants).
- **`packages/core/agent-loop`**: All surface-capable appends pass surface opts. Each `assistant/message` cites its chunk seqs; each `tool/result` cites its `tool/call` seq.
- **`packages/session/session-persistence-sqlite`**: Two new nullable TEXT columns (`source_event_seqs`, `surface_op`) on the `events` table; `SCHEMA_VERSION` bumped (bump-and-reject, no migration).
- **`packages/session/session-persistence-jsonl`**: No changes required.
@@ -6,11 +6,11 @@ English | [中文](2026-06-18-shared-persistence-write-coordinator.zh.md)
## Problem
`dsh-session-persistence-jsonl` and `dsh-session-persistence-sqlite` intentionally prove the same `SessionPersistence` contract over different storage media, but their write-path orchestration was duplicated: per-session state, `session/created` adoption, backend-specific prefix reads, write-behind control, per-id operation serialization, HMR seeding, and dispose drains. The pure seed-prefix collision and serializability guards had already moved into the Service Definition package; the remaining orchestration was still correctness-heavy and received the same fixes twice. Only the storage primitives (write bytes vs. INSERT rows) differed.
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
Extract a backend-agnostic `PersistenceCoordinator` into `dsh-session-persistence`. The coordinator owns the orchestration once; each first-party backend composes one (`new PersistenceCoordinator(ctx, this)`), implements a 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.
`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`.
@@ -24,28 +24,29 @@ The coordinator retires a session from `session/disposed`: it waits for the cont
### The hook interface (`PersistenceBackend<TornMarker>`)
Five required members plus an optional lifecycle hook form the only boundary between the coordinator and storage:
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 (every JSONL project directory; SQLite's id is globally unique). 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 (the materialize-write and the first event batch must commit together — a crash between them must not leave a materialized-but-empty session; this is why there is no separate `materialize` hook).
-`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, SQLite does DELETE+INSERT in one transaction. Used by `prepare`/`load` (truncate + synthetic closers) and live-adoption (truncate only, `closers = []`).
-`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 (SQLite closes its db handle; JSONL omits it), awaited in the dispose effect AFTER the quiescence drain so a close failure never masks a drain error.
-`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 ever tests `tornMarker !== undefined` and passes the value straight back to `commitRepair` — it never inspects it. Each backend picks its own marker type: JSONL carries the byte offset to truncate to plus any complete events decoded from an incomplete final frame, while SQLite carries the seq to delete from. The coordinator therefore knows neither byte lengths nor frame recovery state.
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` (public-API contract) runs for every backend and proves that `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 backend disposal drains, and crash-tail repair through an in-memory reference, JSONL, and SQLite. `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. The per-backend specs retain storage mechanics only. A through-coordinator torn-tail repair test per real backend keeps the opaque-marker branch covered because the contract crash case produces synthetic closers without a torn marker.
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 while SQLite is already id-bound, 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.
- **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 previously duplicated by every backend. 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 backend 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. New backends implement storage primitives rather than copy the bounded write lifecycle.
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.
-`loadStored(id)`——按 id 跨所有存储范围读取一个已存储前缀(JSONL 的所有项目目录;SQLite 的 id 全局唯一)。准备、逻辑加载/检查、物理后缀读取、存活会话接管与创建碰撞探测共用此查找。协调器会断言返回的 id,并在修复或发布状态之前拒绝已存储记录与存活会话的 cwd 不匹配。
@@ -6,47 +6,43 @@ English | [中文](2026-06-20-branded-ids.zh.md)
## Problem
The harness brands `CallId` (`packages/llm/llm/src/brand.ts`) and the shared agent/session `SessionId` (`packages/core/session/src/types.ts`) using the `Branded<B> = string & { readonly [BRAND]: B }`machinery (owned by the type-only`@deepseek-ai/dsh-brand` package at `packages/util/brand/` — see its [README](../../../../packages/util/brand/README.md)) and a zero-cost cast factory per type. `dsh-brand` also states the governing policy: *"Branding is for ids that cross package boundaries and could plausibly be confused; not every string needs a brand."* That policy is right; the problem is that it is only half-applied. Two gaps let a structurally-identical-but-semantically-wrong string slip through the type checker today.
The harness brands `ToolCallId` (`packages/llm/llm/src/brand.ts`) and the shared agent/session `SessionId` (`packages/core/session/src/types.ts`) using `Branded<B> = string & { readonly [BRAND]: B }`and the stateless `brandString<T>()` constructor from`@deepseek-ai/dsh-brand` at `packages/util/brand/` — see its [README](../../../../packages/util/brand/README.md). `dsh-brand` also states the governing policy: *"Branding is for ids that cross package boundaries and could plausibly be confused; not every string needs a brand."* That policy is right; the problem is that it is only half-applied. Two gaps let a structurally-identical-but-semantically-wrong string slip through the type checker.
**Gap 1 — unbranded cross-boundary IDs in the bash seam.** The background-job id is a plain `string`: `BashTask.id: string` (`packages/shell/shell/src/types.ts`), carried as `string` through the whole executor seam (`ShellExecutor.get`/`ownerOf`/`readOutput`/`kill(id: string)` in `packages/shell/shell/src/index.ts`) and validated/passed as `string` by the model-facing tools (`validateJobId`, `assertTaskAccess`, the `job_id` schema arg in `packages/shell/tool-bash/src/index.ts`). It is generated by a per-executor counter — `` `bash-${this.nextTaskId++}` `` in `packages/shell/bash-local/src/index.ts` — which gives it **exactly the same `name-N` shape as `SessionId`'s default** (`` `session-${++counter}` `` in `packages/core/session/src/index.ts`). A bash job id and a session id are trivially swappable at a call site and the compiler says nothing. It is a model-facing id (the model passes `job_id` back to `bash_output`/`bash_kill`), so a confusion here is reachable from untrusted input.
The bash **owner token** is the related sub-case: `ShellExecRequest.owner?: string` and `ShellExecSpec.owner: string | undefined` (`packages/shell/shell/src/types.ts`) are documented as a deliberately *opaque* isolation key, but in every live caller the value IS the owning agent's shared `Agent.id`/`SessionId` (`callerToken = (exec) => exec.agent?.id` in `packages/shell/tool-bash/src/index.ts`) wearing a different seam-local name. It is compared for access control (`owner !== callerToken(exec)`), so a mismatched-but-well-typed string here is a cross-session isolation bug the type system currently cannot catch. This is the shared id alias covered by the [unified agent/session identity decision](../simplification/2026-06-20-unify-agent-and-session-id.md).
**Gap 2 — brand erosion at the boundaries of the *already-branded* IDs.** Even `CallId` and `SessionId` decay back to bare `string` at exactly the places confusion is most likely: registry/store key types and public method params. Representative sites include the session store, the agent registry (both keyed by the shared `SessionId`), tool-presentation call-id maps, ACP's session records, and the persistence coordinator. A brand that is dropped at a collection key buys nothing on lookups — the value of the existing brands is partly unrealized.
**Gap 2 — brand erosion at the boundaries of the *already-branded* IDs.** Even `ToolCallId` and `SessionId` decay back to bare `string` at exactly the places confusion is most likely: registry/store key types and public method params. Representative sites include the session store, the agent registry (both keyed by the shared `SessionId`), tool-presentation call-id maps, ACP's session records, and the persistence coordinator. A brand that is dropped at a collection key buys nothing on lookups — the value of the existing brands is partly unrealized.
## Decision
A type-only change. Brands are zero-cost casts; nothing about runtime behavior, serialization, comparison, or the wire format changes. The decision has three parts, all honoring the existing "not every string" policy.
Brands remain ordinary strings; `brandString<T>()` returns its input unchanged, so serialization, comparison, and wire formats do not change. The decision has three parts, all honoring the existing "not every string" policy.
- **Brand the bash job id.** Add `BashTaskId = Branded<'BashTaskId'>` plus its same-named factory in `packages/shell/shell/src/types.ts` (the package that *owns* the id), importing `Branded` from `@deepseek-ai/dsh-brand` exactly as `SessionId` does. The brand primitive lives in the dependency-free `dsh-brand` utility package precisely so `dsh-shell` can brand its ids by depending on it alone — it never pulls in `dsh-llm` (or `dsh-session`) just to reach `Branded`. Thread it through `BashTask.id`, the `ShellExecutor` Service Definition methods (`get`/`ownerOf`/`readOutput`/`kill`), the generation site in `dsh-bash-local` (brand the counter output once, at creation), and the `dsh-tool-bash` validate/access surface (`validateJobId` returns a `BashTaskId`; `job_id` is branded at the tool boundary where the model's string arrives).
- **Brand the bash job id.** Add `BashTaskId = Branded<'BashTaskId'>` in `packages/shell/shell/src/types.ts` (the package that *owns* the id), importing `Branded` and constructing values with `brandString<BashTaskId>()` from `@deepseek-ai/dsh-brand`. The brand utility exists so `dsh-shell` can brand its ids by depending on it alone — it never pulls in `dsh-llm` or `dsh-session` just to reach the primitive. Thread the type through `BashTask.id`, the `ShellExecutor` Service Definition methods (`get`/`ownerOf`/`readOutput`/`kill`), the generation site in `dsh-bash-local`, and the `dsh-tool-bash` validation/access surface.
- **Mint a distinct `OwnerToken` brand.** Add `OwnerToken = Branded<'OwnerToken'>` in `packages/shell/shell/src/types.ts`; type `ShellExecRequest.owner` / `ShellExecSpec.owner` / `ShellExecutor.ownerOf` as `OwnerToken | undefined`. The `dsh-tool-bash` consumer casts the agent's shared `id` (`SessionId`) into an `OwnerToken` at the boundary — the one place the two vocabularies meet. The bash Service Definition never imports `dsh-session`. (Rationale in the next section.)
- **Mint a distinct `OwnerToken` brand.** Add `OwnerToken = Branded<'OwnerToken'>` in `packages/shell/shell/src/types.ts`; type `ShellExecRequest.owner` / `ShellExecSpec.owner` / `ShellExecutor.ownerOf` as `OwnerToken | undefined`. The `dsh-tool-bash` consumer applies `brandString<OwnerToken>()` to the agent's shared `id` (`SessionId`) at the one place the two vocabularies meet. The bash Service Definition never imports `dsh-session`. (Rationale in the next section.)
- **Stop the brand erosion.** Propagate the existing brands to the `Map` key types and public method params listed under Gap 2 — `Map<SessionId, Session>`, `Map<SessionId, Agent>`, `get(id: SessionId)`, `Map<CallId, …>`, ACP's `SessionId` surface, and the coordinator's `Map<SessionId, …>`. This is the larger mechanical share of the change and the part that makes the *existing* brands actually load-bearing on lookups, not just on struct fields.
- **Stop the brand erosion.** Propagate the existing brands to the `Map` key types and public method params listed under Gap 2 — `Map<SessionId, Session>`, `Map<SessionId, Agent>`, `get(id: SessionId)`, `Map<ToolCallId, …>`, ACP's `SessionId` surface, and the coordinator's `Map<SessionId, …>`. This is the larger mechanical share of the change and the part that makes the *existing* brands actually load-bearing on lookups, not just on struct fields.
Illustrative shape (the factory pattern is identical to the three existing brands):
Illustrative shape:
```ts ignore-check
import type { Branded } from '@deepseek-ai/dsh-brand'
import { brandString, type Branded } from '@deepseek-ai/dsh-brand'
/** A background bash task handle (generated `bash-N` by the local executor). */
export type BashTaskId = Branded<'BashTaskId'>
export function BashTaskId(id: string): BashTaskId {
return id as BashTaskId
}
const taskId = brandString<BashTaskId>('bash-1')
/** A bash task's opaque isolation key — the consumer's owner identity, NOT the bash seam's. */
export type OwnerToken = Branded<'OwnerToken'>
export function OwnerToken(id: string): OwnerToken {
The obvious shortcut is to type `owner` as `SessionId` directly — it always *is* one. We reject that. The bash executor seam is a capability seam (Service Definition `dsh-shell`, Service Provider `dsh-bash-local`, Consumer `dsh-tool-bash`) and its owner token is *documented as deliberately opaque*: the executor "never interprets it (no access policy lives in the seam — that is the consumer's job)" (`packages/shell/shell/src/types.ts`). Typing the Service Definition's field as `SessionId` would import `dsh-session`'s vocabulary into a package that must not know what an owner token *means* — it would couple a generic execution backend to the session model and contradict the opaque-token design. A sandboxed or remote executor that replaces `dsh-bash-local` should not inherit a session dependency. The distinct `OwnerToken` brand keeps the seam decoupled: `dsh-shell` knows only "an owner is some opaque branded token," and the `dsh-tool-bash` consumer — which already decides the access policy — is the single boundary that casts its `SessionId` into an `OwnerToken`. The brand still delivers the safety win (you cannot pass a `BashTaskId` or a raw string where an owner is expected) without the coupling.
The obvious shortcut is to type `owner` as `SessionId` directly — it always *is* one. We reject that. The bash executor seam is a capability seam (Service Definition `dsh-shell`, Service Provider `dsh-bash-local`, Consumer `dsh-tool-bash`) and its owner token is *documented as deliberately opaque*: the executor "never interprets it (no access policy lives in the seam — that is the consumer's job)" (`packages/shell/shell/src/types.ts`). Typing the Service Definition's field as `SessionId` would import `dsh-session`'s vocabulary into a package that must not know what an owner token *means* — it would couple a generic execution backend to the session model and contradict the opaque-token design. A sandboxed or remote executor that replaces `dsh-bash-local` should not inherit a session dependency. The distinct `OwnerToken` brand keeps the seam decoupled: `dsh-shell` knows only "an owner is some opaque branded token," and the `dsh-tool-bash` consumer — which already decides the access policy — is the single boundary that applies `brandString<OwnerToken>()` to its `SessionId`. The brand still delivers the safety win (you cannot pass a `BashTaskId` or a raw string where an owner is expected) without the coupling.
## Out of scope / possible extensions
@@ -56,14 +52,14 @@ Kept deliberately narrow per the "not every string needs a brand" policy. Each o
- **`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.
- **Validated construction** — the brand factories are pure casts with no runtime check, and every boundary (ACP `sessionId`, provider-issued `call.id`, the empty-string fallback in `dsh-llm-deepseek`) trusts the raw string today. 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, not bundled into this type-only change.
- **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.
## Verification
The landed invariants: `BashTaskId` and `OwnerToken` are defined in `dsh-shell` and threaded end-to-end (Service Definition, the `dsh-bash-local` generation site, the `dsh-tool-bash` model-facing tool) with no `dsh-shell` dependency on `dsh-session`; no collection keyed by an in-scope branded id (`CallId`/`SessionId`/`BashTaskId`) is keyed by bare `string`; public method params and exported signatures keep the brand; and brands are constructed via the cast factory at each boundary where a raw string enters (provider call id, ACP session id, model-supplied `job_id`), never as scattered `as` casts.
The landed invariants: `BashTaskId` and `OwnerToken` are defined in `dsh-shell` and threaded end-to-end (Service Definition, the `dsh-bash-local` generation site, the `dsh-tool-bash` model-facing tool) with no `dsh-shell` dependency on `dsh-session`; no collection keyed by an in-scope branded id (`ToolCallId`/`SessionId`/`BashTaskId`) is keyed by bare `string`; public method params and exported signatures keep the brand; and boundaries where raw strings enter use `brandString<T>()` rather than scattered `as` casts.
## Consequences
- **Mechanical churn across two surfaces.** Propagating brands touches the bash seam (Service Definition + Service Provider + Consumer) and the ACP session-id surface plus the persistence coordinator. The churn is broad but low-severity: a missed site is a compile error, not a silent bug. The change is observably type-only — no snapshot or e2e behavioral diff. It sits next to the [unified agent/session identity decision](../simplification/2026-06-20-unify-agent-and-session-id.md) because both touch the session-id / owner-token boundary; `OwnerToken` stays distinct from the unified id for the decoupling reason above.
- **Mechanical churn across two surfaces.** Propagating brands touches the bash seam (Service Definition + Service Provider + Consumer) and the ACP session-id surface plus the persistence coordinator. The churn is broad but low-severity: a missed site is a compile error, not a silent bug. Construction returns the same runtime string, so there is no snapshot or e2e behavioral diff. It sits next to the [unified agent/session identity decision](../simplification/2026-06-20-unify-agent-and-session-id.md) because both touch the session-id / owner-token boundary; `OwnerToken` stays distinct from the unified id for the decoupling reason above.
- **Brands do not validate.** A brand is a confusability guard, not a correctness proof: a *wrong* session id that is still a well-formed string passes the type checker exactly as before. This decision does not close that gap (see Out of scope) — it only stops the *category* error of passing the wrong *kind* of id.
- **The "where to stop" line stays a judgment call.** Branding `BashTaskId` but not `ToolName`, `OwnerToken` but not `ModelId`, is a taste call about which strings "could plausibly be confused." Reasonable reviewers may want more or fewer; the policy in `brand.ts` is the tie-breaker, and this decision errs toward the ids that are model-facing or used for access control.
Loaded 100 of 7079 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.