New doc-sync gate verify-export-jsdoc walks every module-level exported
name under packages/*/*/src and requires description prose everywhere,
plus @param per parameter and @returns on non-void annotated returns for
function-like exports, public class methods, properties, and accessors.
The parsing + check helpers move out of gen-cordis-catalog.ts into a
shared scripts/jsdoc.ts so 'documented' means one thing on both gated
surfaces.
Deliberate exemptions (documented in the RFC): heritage-declared class
members (the seam declaration is the doc's one home — the one checker
query in an otherwise pure-AST walk), cordis plugin-protocol slots
(name/inject/reusable/Config/apply, top-level and static), constructors,
overload implementations, declare-module augmentation bodies, and
re-export statements (checked at the defining module).
The 203 under-documented exports the gate found at adoption are filled
in this change, so the gate lands green; generated catalogs/graphs are
regenerated for the shifted line pointers.
RFC: docs/rfc/implemented/process/2026-07-06-export-surface-jsdoc-gate.md
gen-cordis-catalog now hard-errors (aggregated, not fail-fast) when an
event lacks description prose or a payload @param, or a public service
method lacks JSDoc, a @param per parameter, a @returns on a non-void
result, or an explicit return type annotation. The this receiver and the
trailing waterfall next are exempt on events (mode machinery owned by
@mode); a stale @param naming no real parameter errors, mirroring the
@mode contradiction check. parseJsDoc now ends prose at the first block
tag (standard JSDoc semantics), so the tags never change the rendered
catalog — only Source: line pointers moved.
Fills the ~139 gaps found across the 15 surface files, extends the spec
with negative-path fixtures for every new guard plus the exemptions,
records the decision as an implemented process RFC, and extends the
AGENTS.md typed-events bullet with the authoring rule. Runs inside
verify-cordis-catalog -> doc-sync, so CI and pre-push enforce it with
zero new wiring.
The audit swept every packages/*/* plugin for the new AGENTS.md
convention (no hardcoded tunables in plugins) and exposes each finding
as a defaulted, validated Config field. Defaults are the previously
hardcoded values throughout, so no deployment or golden changes.
- tool-fs (had NO Config): readLimit, readMaxLineLength, readMaxBytes,
readStreamMinSize. The caps thread through ReadToolCaps/ReadWindow —
read-render already documented that the consumer applies the caps, so
they become explicit per-request fields.
- tool-web: searchMaxResults (WEB_SEARCH_MAX_RESULTS stays as the
schemastery default). Also fixes the stale GREP_LIMIT references in
search.ts and the web-capability-seam RFC (no such constant exists).
- bash-local: graceMs (SIGTERM->SIGKILL escalation grace). The
RunInternals.graceMs test seam is gone: graceMs is now a required
SpawnSpec field filled from config, so tests exercise the real
config path and the defaults live in exactly one place.
- subagent-acp: disposeEofGraceMs / disposeGraceMs. The AcpRunSpec
fields become required for the same one-defaulting-layer reason.
- session-persistence-sqlite: journalMode ('wal' default; the
rollback-journal modes serve filesystems where WAL's shared-memory
files do not work, e.g. network mounts).
- hooks-claude + hooks-codex: stderrSummaryMaxChars for the persisted
hook/result stderr summary. The duplicated summarize() helpers merge
into hook-protocol's summarizeStderr(stderr, maxChars), beside the
HookResultRecord field it feeds, with the bound parameterized the
same way runHook's defaultTimeoutMs already is.
- compact-basic: charsPerToken for the token estimator (default 4, the
English-text heuristic; CJK-heavy deployments need ~1-2 or compaction
fires far too late). Also corrects the BasicCompactService class doc,
which claimed defaults the required-field config never had.
- fs-local: deletes the dead STREAM_MIN_SIZE constant and the dead
FsIoInternals.streamMinSize seam — the read-routing bound lives in
the consumer (tool-fs), where it is now config. This is item 1 of
the proposed prune-write-only-fs-surface RFC, annotated accordingly.
Every new field gets range validation (following the existing
assertPositiveFinite pattern), a README row, and tests covering the
configured behavior, the schema default, and load-time rejection.
P1: both merge parents shipped SCHEMA_VERSION=3 for different layouts (surface
columns vs seed_length), so an on-disk 3 was ambiguous and wrongly accepted.
Bump to 4 (merged layout) so the version check rejects both sibling v3s.
P2: a surface-eligible event with no surfaceOp lands in the log but vanishes
from deriveMessages() (surface is the sole derivation path). The typed append
overload enforces the marker only when the type arg is a literal; it collapses
to optional when widened to the union (a caller iterating raw events). Guard at
runtime in both append() and the seed constructor — no backward-compat for
surface-less logs. Shared seed fixtures carry surfaceOp explicitly and the
appendLog helper forwards it verbatim (no synthesized default). Exports
isSurfaceEligibleType. Regression tests for all three, each verified to fail
on the unfixed code.
Gates: typecheck, test (1115), snapshot (14), doc-sync, lint, build, hygiene green.
Reconciles the session-surface work (surfaceOp/sourceEventSeqs provenance as
the sole derivation path) with master's worktree-subagent series (fork-seed
boundary + out-of-process subagent backends).
Semantic reconciliations beyond the textual auto-merge:
- SQLite SCHEMA_VERSION: both sides bumped 2->3. Merged to a single v3 carrying
BOTH column families — master's seed_length on `sessions` and surface's
source_event_seqs/surface_op on `events`. writeRow + both INSERT sites bind
the full set; the schema doc lists all three added columns as the v2->v3 gap.
- agent-loop runStep request: master's `sessionId: session.id` and surface's
per-append surfaceOp/sourceEventSeqs coexist (different regions).
- Fork seed + surface: a fork seeds the child from the parent's LIVE events,
which now carry surfaceOp, so the child's surface rebuilds correctly. Verified
end-to-end — the subagent-fork replay recalls the inherited "SAFFRON" codeword
through the seeded prefix.
- Subagent snapshot fixtures (recorded pre-surface) re-enriched via KEYLESS
deterministic replay: only surfaceOp/sourceEventSeqs added onto existing
recorded lines (matched by seq), no recorded value changed. Not re-recorded
against the live API.
Gates: typecheck, test (1112), test:snapshot (14), doc-sync, lint, build,
hygiene all green.
A fork subagent seeds its child session with a prefix of the parent's log, and
that seed becomes the child's persisted log — so a fork child's .jsonl begins
with the PARENT's events, including the parent's assistant/chunk events. The
snapshot replay harness derived a child's script from its whole log, which would
replay the parent's recorded responses as the child's model calls. Spawn-only
scenarios never hit it, but a fork snapshot would mis-route silently.
Record the seed boundary and skip the inherited prefix at replay:
- SessionHeader gains an optional `seedLength` (how many leading events were
inherited via a seed), threaded through CreateSessionOptions/CreateAgentOptions
meta and stamped by the fork backend (= seeded-prefix length; absent for spawn).
It is EXPLICIT, never inferred from seed.length: a resume seeds the whole stored
log, so the resume path passes the persisted boundary back.
- Both persistence backends round-trip it: JSONL header line, SQLite seed_length
column. The SQLite table change bumps SCHEMA_VERSION 2->3; per the pre-release
stance the backend rejects an older user_version on open with NO migration.
- llm-replay's parseSessionHeader reads seedLength and loadSessionScripts derives
a child script from events AFTER the boundary. seedLength is 0 for spawn, so
spawn replay is byte-for-byte unchanged.
Closes the routing-correctness gap the per-session snapshot replay RFC under-
stated; a recorded fork scenario remains a future addition but now derives
correctly. RFC: docs/rfc/implemented/testing/2026-06-22-fork-child-replay-seed-boundary.md.
Regression coverage: a fork child fixture whose seeded prefix carries a parent
chunk (derived script must exclude it, proven red without the slice); a seedLength
persistence round-trip through the shared coordinator contract (both backends);
the fork backend stamping it; resume preserving it from the persisted header.
Reconcile the session-surface feature with master's package reorg and
simplifications:
- Adopt master's folded usage (assistant/message.usage; standalone `usage`
event dropped) and re-attach surface metadata (surfaceOp/sourceEventSeqs).
- Add surface opts to master's new max-tokens assistant/message append.
- Port surface columns onto the coordinator-refactored SQLite backend at its
new path; drop the dead v1->v2 migration (bump-and-reject, no migration per
pre-release policy).
- Move the session-surface RFC into implemented/architecture/ and refresh its
stale body (no migration, SESSION_FORMAT_VERSION=0, renamed package paths).
- Update the core-data-structures catalog SessionEvent blocks for the two new
surface fields; regenerate the cordis catalog.
- Re-harvest ACP snapshot fixtures (keyless replay) to carry surface metadata.
Codex review of the trace-event fold found two merge-blockers.
Blocker #1 — format version. Folding usage onto assistant/message and removing
the standalone usage/error events changed the persisted SessionEventMap shape,
which per the AGENTS.md "bump the version and reject — don't migrate" policy
requires a backend to reject any non-current log. Centralize the version in an
exported SESSION_FORMAT_VERSION constant (dsh-session), read by both write sites
(Session constructor default, SessionStore.prepare header) and the coordinator's
load-time assertVersion check. The constant is pinned at 0: while unreleased the
on-disk format is unstable/pre-release, so breaking shape churn is absorbed at v0
(no monotonic bump until the first tagged release) and any non-0 log is rejected
on load — no migration. Update every test/fixture/doc that stamps a
currently-written header to the constant, bump the ACP snapshot fixture + golden
headers to v0, and keep the version-rejection test meaningful by switching its
bad value to a clearly non-current 99. AGENTS.md documents both the monotonic
(SQLite SCHEMA_VERSION) and pinned-0 (session log) pre-release stances.
Blocker #2 — restore the late turn-end warn. failTurn now sets the error reason
only while the turn is still open; once turn/end is appended (a throwing
agent/turn-end listener after closeTurn) the reason can no longer reach the
durable log, so the late throw is logged via ctx.logger.warn instead of
vanishing into a futile post-close assignment. A regression test asserts the
warn fires.
Also guard the normal-step assistant/message append with the same
content-or-usage condition as the max-tokens branch (a content-less, usage-less
step records no trace-only row), with a covering test.
Type-only change (brands are zero-cost casts; no runtime/wire impact). Closes
the two gaps in the "brand ids that cross package boundaries" policy and fixes
the dependency direction so a capability package never pulls in an unrelated one.
- Extract the `Branded<B>` primitive into a new standalone type-only package
`@deepseek-ai/dsh-brand` (packages/util/brand) with no harness-package deps.
dsh-llm keeps its owned CallId but imports Branded from dsh-brand; dsh-session,
dsh-agent, and dsh-bash all import Branded from there. dsh-bash depends on
dsh-brand ALONE — never on dsh-llm or dsh-session (the architectural fix: a
generic execution backend must not couple to the LLM or session vocabulary).
- Mint BashTaskId + OwnerToken in dsh-bash and thread them through BashTask.id,
the get/ownerOf/list/readOutput/kill seam, the bash-local generation site, and
the dsh-tool-bash validate/access surface. OwnerToken is a DISTINCT brand from
SessionId so the seam stays decoupled; dsh-tool-bash is the single boundary
that casts SessionId -> OwnerToken.
- Brand at the SOURCE, not via mid-pipeline casts: agent-loop's Config types
agents[].id as AgentId and resumeSessionId as SessionId, so the brand enters
at the config boundary and the inner create()/resume casts disappear (only the
genuinely-new per-run session-id string is cast).
- Stop brand erosion: propagate CallId/SessionId/AgentId to the registry/store
Map keys and public params/exports (SessionStore, AgentRegistry + factory
options, the ACP session-id surface + ToolPresenter CallId map, the
persistence coordinator, invariants pendingCalls, the pi-ai tool-call maps).
- Docs: document BashTaskId/OwnerToken in bash.md (type-equiv re-pasted), point
the Branded type-equiv at dsh-brand, fix stale param types in the session/
agent/bash READMEs, regenerate the cordis catalog + module graph.
Implements docs/rfc/proposed/architecture/2026-06-20-branded-ids.md
Codex review of the PR2 diff caught doc/comment sites doc-sync does not gate
(core-data-structures prose) and a latent test-helper race:
- docs/core-data-structures/persistence.md + bash.md, sqlite README, and two
source comments (coordinator.ts, jsonl.spec.ts) still listed the removed
has/delete/get/list methods — updated to the surviving four-method
persistence surface and the get/list-free bash seam.
- doneFor(ctx, id) attached its onTaskDone listener lazily, after the task
could already have closed (e.g. `true`), so it could miss the completion and
hang. Replaced with trackCompletions(ctx): one eagerly-installed listener
(mounted in setup() before any task starts) records every completion, and
doneFor resolves immediately for an already-finished task or on completion
otherwise. Race-free, and there is no get-by-id seam left to poll instead.
Two capability seams carried abstract methods no production consumer calls.
A method no consumer programs against is not a seam — it is speculative
surface every implementation must still provide and test.
- SessionPersistence: remove has() and delete(), the coordinator's
has/delete/deleteCore, and the PersistenceBackend.deleteStored hook (with its
jsonl + sqlite + in-spec memory-stub impls). Surviving service surface:
create/append/load/list. Production uses only load() (resume) and list()
(ACP session/list).
- BashExecutor: remove get(id) and list(), the abstract decls and the
LocalBashExecutor impls. The internal tasks map survives (it backs
ownerOf/readOutput/kill); get/list were pure public accessors over it with no
shipping caller and no bash_list tool.
- Migrate tests that reached through ctx.bash.get(id) to the public completion
seam: a doneFor(id) helper over onTaskDone awaits a task by id, and the
HMR-reload ownership test now proves task survival through A's own bash_output
([status: running]) plus ownerOf + B-rejection — a stronger through-the-tool
assertion than the removed lookup peek.
- Update seam READMEs (six -> four service methods, drop the deleteStored hook
and the get/list row) and the two implemented persistence RFCs in place.
Implements docs/rfc/implemented/simplification/2026-06-20-prune-dead-seam-methods.md
Add a README to each group dir (core/llm/bash/session-persistence/ui/
support) stating its role and product-vs-support classification, and
rewrite packages/README.md around the hierarchy (group table, grouped
"what goes where", removed the package-hierarchy FIXME).
Move the package-hierarchy RFC to implemented/architecture/ and rewrite
it to describe what shipped (placement rationale, the paths-wildcard and
publint dedup, the two new guardrail gates). Fold the remaining
tsconfig.build.json references dedup into the discover-package-inventory
proposal and fix its cross-link.
Update AGENTS.md: regrouped repo-layout map, depth-2 globs, the new
verify-package-paths gate in the doc-sync listing, and a note that we
lean toward stricter lint in the agentic-coding era (machine-caught
errors and a consistent foundation outweigh the one-time cost).
Merge brought in the RFC-classification reorg and two new doc gates;
rewrite every drifted packages/<name> cross-link (Markdown link targets,
moved-README relative depths, and .ts comment paths) to the grouped paths.
Add two doc-sync/hygiene gates so the manual checks this restructure
needed become automated:
- verify-package-paths.ts: flags a packages/<path> reference (in Markdown
or a .ts comment/string) that does not resolve AND names a real package
in a segment — i.e. a stale path to a MOVED package. A path naming a
non-existent package (a forward-looking proposal) is left alone, so it
applies uniformly across proposed/implemented/rejected.
- check-workspace-constraints: assert the packages/<group>/<pkg> depth-2
shape (group dirs carry no package.json; no flat or over-nested
packages). Group names stay open; only the shape is fixed.
Move the 18 flat packages/<name> packages into role-grouped dirs:
core/, llm/, bash/, session-persistence/, ui/, support/. Group dirs are
pure containers; each package keeps its @deepseek-ai/dsh-* name.
Collapse the per-package tsconfig paths maps (base + typecheck) into one
@deepseek-ai/dsh-* wildcard with a candidate per group, and derive the
publint list from the hierarchy. Update all depth-coupled globs/configs
(workspace, tsdown, vitest, eslint, knip, tsconfig includes/refs,
per-package tsconfigs, generators, doc-script scopes, type-equiv manifest)
and the cross-package/script relative imports in tests.
Fix doc-typecheck's workspacePaths() to parse tsconfig JSONC via the
TypeScript API instead of a regex comment-strip, which corrupted the
new wildcard `/*/` path candidates.
WIP: doc cross-links and package/RFC docs still to update.
Add a second axis to every RFC — its class (feature, bug-fix,
simplification, architecture, process, testing) — encoded in the path
as docs/rfc/{lifecycle}/{class}/file.md. The folder is the label, so
the closed set is enforced by structure rather than a parsed field.
Two new doc-sync gates back it:
- verify-rfc-classification: every RFC sits in a valid class folder and
the README index lists it under the matching lifecycle→class heading.
- verify-doc-refs: every docs/*.md path cited in a packages|examples TS
comment resolves — closes a drift class verify-md-links can't see, and
catches the four comment refs this reorg moved.
The README gains a Classification section explaining the taxonomy and
per-class index sub-sections. A self-referential process RFC records why
the scheme is path-encoded and gated.
Reword the suite's module doc to describe what it IS — each scenario lives
here once and runs per backend through the fixture — rather than narrating
that the scenarios were previously duplicated in the per-backend specs. Per
the repo doc-current-state convention (no process/history in comments).
A reviewer found a cross-cwd hole: the ownerless-state claim path validated only
the seed prefix (via loadStored, any scope) and never compared the tracked
header's cwd to the live session's. So an ownerless `create(meta(id, "/a"))`
with cursor 0 (seed matches trivially) was claimed by a live session with the
same id at cwd "/b", and the "/b" events then appended under the "/a" header —
bypassing the cwd-scoped loadLive() guard that the HMR-adopt path (case 2) uses.
Add a cwd equality check before the seed check in the ownerless-claim branch: a
same-id ownerless artifact at a different cwd is a collision, not a claim. This
is a coordinator-level invariant (the live session's cwd must match the tracked
meta's cwd) and applies to both backends.
Tests (shared coordinator contract, run per backend): a live session at a
different cwd cannot claim cursor-0 ownerless state, cannot claim a
loaded-prefix even when the seed matches, and a no-cwd state cannot be claimed
by a cwd'd session. All fail without the guard.
Also documents WHY the `materialized` flag is needed (lazy create leaves no
artifact; it distinguishes registered-but-unwritten from durably-present for
has()/reclaim) and reframes the module doc to current-state, not the refactor
history (per the new AGENTS.md doc convention).
Codex's converge pass on PR B found a cross-cwd adoption hole: the coordinator
calls loadLive(id, session.header.cwd) for HMR live-adoption, but JSONL's
loadLive delegated to findLog(id, cwd) which, for cwd === undefined, scanned
ALL cwd buckets. So a live NO-CWD session could adopt a same-id log from a real
cwd bucket, ending with a live cwd: undefined but a persisted meta.cwd: '/w'.
loadLive must treat `undefined` as the DEFINITE no-cwd bucket, not "unknown":
it now goes straight to logPath(cwd, id) (which maps undefined -> _no-cwd),
never the all-buckets scan. loadStored/deleteStored keep the any-cwd scan
(resume/removal identify by id alone), so findLog is now a pure scan-all and
loses its dead cwd-direct branch.
The coordinator's has() relied on loadLive(id, undefined) meaning "any scope"
for an untracked id — fixed to use loadStored for the untracked (unknown-cwd)
case and loadLive only for a tracked session's known cwd.
Adds a regression test: a no-cwd live session reusing an id persisted in a real
cwd bucket no longer cross-cwd-adopts — it falls through to createCore's
any-cwd collision probe and REJECTS, leaving the original log untouched. Also
fixes the README to say `tornMarker !== undefined` (a marker may be falsy, 0).
The JSONL and SQLite backends were byte-identical (or same-algorithm) for ALL
of their write-path orchestration — the four maps (states/buffers/chains/inits),
installWritePath, initFor, onCreated's four adoption cases, flush, drain,
serialize, adopt/adoptLivePrefix, assertVersion, and the create/append/load/
has/delete skeletons. Only the storage primitives (write bytes vs INSERT rows)
differed, so every fix landed twice.
Extract that orchestration into a PersistenceCoordinator in the seam package.
Each backend composes one (new PersistenceCoordinator(ctx, this)), implements a
small PersistenceBackend hook interface (loadStored, loadLive, appendBatch,
commitRepair, deleteStored, list, optional close), and delegates its six public
service methods to it. Composition, not inheritance — a backend exposes only the
hooks, can't reach the coordinator's private state, and the public
SessionPersistence API is unchanged so a third-party backend may still implement
it directly.
The crash-repair torn-tail token is OPAQUE: the coordinator computes the
synthetic closers (it owns interruptedTurnClosers) but only tests
`tornMarker !== undefined` and round-trips it to commitRepair, never inspecting
it (JSONL = byte offset, SQLite = seq). loadStored vs loadLive stay distinct so
HMR adoption is cwd-scoped (a same-id log at a different cwd is a collision, not
a resume). appendBatch carries meta so lazy-materialize + first-batch commit
atomically (no separate materialize hook).
Tests: the duplicated orchestration tests (adoption, HMR, collision,
dispose-drain, crash-tail) move into one runCoordinatorContract suite run once
per backend (memory + jsonl + sqlite) via hook fixtures; per-backend specs keep
only storage mechanics. A through-coordinator torn-tail test per real backend
keeps the commitRepair-with-marker branch covered under the 100% gate.
Net -112 lines (the dedup outweighs the new coordinator + shared suite); 100%
coverage; backends shrank ~1200 lines of duplicated churn. Migrates the
write-coordinator RFC proposed -> implemented.
SessionSummary (updatedAt/title/firstPrompt) and SessionPersistence.update()
were dead state: zero production callers of update(), no production reader of
updatedAt/firstPrompt, and ACP's title comes from a tool-call presenter, not
storage. The live Session.header was already typed SessionHeader, so the
summary only ever existed in the persistence layer, written and read by nothing
but its own contract test.
Delete it entirely (no SessionMeta alias — SessionMeta collapses to
SessionHeader everywhere). This removes the JSONL .summary.json sidecar
machinery, the SQLite title/first_prompt/updated_at columns and per-append
updated_at bump, and the update() method from the abstract service and both
backends. SQLite SCHEMA_VERSION goes 1->2 and openDatabase now rejects any
non-current user_version (older or newer) — no migration, unreleased software.
Net -400 lines, and it erases the JSONL-sidecar-vs-SQLite-column durability
divergence that the upcoming write coordinator would otherwise have to model.
Records the decision in docs/rfc/implemented/2026-06-19-drop-mutable-session-summary.md
and migrates the 2026-06-14 session-persistence RFC's facts to current truth.
Adds a standalone AGENTS.md section "Tests document behavior, not golden truth"
(a passing test pins current behavior, not necessarily correct behavior) with
the summary-drop as its worked example, and reinforces the no-migration
pre-release stance.