Write handles now hold a durable lease (session.lock.json beside the
log): a random owner token, diagnostic pid, and an expiry. Acquisition
wins by exclusive create; a second process's create or write open rejects
while the record is unrenewed for less than leaseTtlMs (default 5 min),
and takes over after that — a crashed holder is waited out, never
reclaimed by pid. The holder renews every leaseRenewIntervalMs (default
4 min); a renewal that finds a foreign, vanished, or expired record — or
fails outright — marks the lease lost permanently, so every later
append/flush rejects with SessionOwnershipLostError while reads continue.
Close releases the record; read handles never touch it. Takeover of an
expired record is eventually exclusive: a replaced holder stops within
one renewal interval.
Refs #3245
The dsh-family version rule lived only in the packages/ block of
checkWorkspaceManifest, so apps/ members and the root-named CLI carried the
shared version with no static check of their own. The name-based rule now
covers every scanned manifest; regression coverage names the stale-version
merge that shipped packages/util/http-proxy at 0.1.2-alpha.5 behind a
0.1.2-rc.1 family.
`session[.vN].jsonl` was assembled independently by the JSONL provider
(twice), the session-log export archive, and the recorded-session fixture
helpers. `dsh-session-format` now owns `sessionFormatLogFilename` and
`parseSessionFormatLogFilename`; the three consumers append only their
compression suffix, and the migration note names the owner.
`snapshotSessionFormatJson` re-implemented the lossless JSON walk that
`dsh-util-values` already publishes as `snapshotJsonValue` + `deepFreeze`,
and the frozen v0 relationship validator carried a third structural JSON
comparison next to `deepEqualJson`. Both now delegate; the format package
reports one `is not lossless JSON` diagnostic per labelled subject instead
of ten member-specific ones, and enumerable accessors whose values survive
a JSON round trip are accepted like `JSON.stringify` accepts them.
The continuable-child headless expectation and the Python `advanced` and
`restart` scenarios pin the current runtime, whose subagent children carry no
`session/end-seed` marker and whose assistant messages embed their streams:
the child fixture drops the marker and renumbers its references, `advanced/`
re-records `result.json`, and both scenarios gain their v2 Session fixtures.
Unit tests cover the `tool-call-delta` id and name validation, embedded
stream members without timing arrays, and the versionless-header refusal.
`session-log-export` names its archive entry after `SESSION_FORMAT_VERSION`,
a runtime value of the shared `dsh-session` instance, so `dsh-session` becomes
a peer dependency and the export is classified as peer-required in the package
dependency policy. The three Session format packages take the `0.1.2-rc.1`
root version that master now requires.
The repository-only benchmark, its unit-suite spec, the root script, and the
README section are removed; the accepted acceptance run's figures stay in the
embedded-assistant-streams note as recorded facts. The note also records why
the migration edge reuses the `dsh-llm` stream helpers instead of frozen
copies and why its target validation re-checks message/stream agreement
itself. A validation test that recomputed `RELEASED_V2_EVENT_TYPES` from the
same expression as its definition is dropped.
An owned job cannot start without the agent registry, so the plugin only
needs it to resolve a notice destination; requiring it left compositions
that run unowned jobs — the PTC harness, the pwsh loader and integration
suites — without an attached controller. The inject list, catalogs, and
README return to the pre-refactor requirement set.
- docs/subsystems/jobs.md (+zh) records JobSpec, JobHandle, JobHooks,
JobOutcome, the pull-source types, JobChunk, JobOutputRead, JobView,
JobRead, VisibleJobs, and the event types as type-equivalence blocks; the
manifest and the cordis catalog link map follow
- the jobs, jobs-local, and tool-jobs READMEs (+zh) describe the ring, the
caller-bound view, the event stream, and the notice ledger; the
architecture table and the adding-a-tool cookbook drop record: true and
readOutput
- Agent Note 2026-09-03-jobs-seam-consolidation owns the decision; the
2026-09-01 record note and the 2026-08-08 web display note carry
Superseded pointers and the display note links the rows spec
- regenerated catalogs (cordis, api, inspect, client, tool, config,
persistence, doc graphs, module graph) with their Chinese mirrors; the
cordis catalog drops the jobOutput exemption and the tool catalog mounts
the agent registry for tool-jobs; the client model imports JobView from
its leaf so the typert analyzer resolves it
Spec files that create /tmp/dsh-* directories via mkdtemp now track and
delete them in afterEach/afterAll; module-scope fixture dirs (executor
spill dirs) are removed in afterAll. The file list came from the
observed-residue inventory on the self-hosted CI host: only specs whose
dirs actually accumulated were leak sources (issue #3134), superseding
the kept-but-unmerged CI sweep branch per the #3233 review decision.
Product per-process spill roots (dsh-subprocess-local spawn,
dsh-spill-local store) register a process-exit handler that removes the
memoized dir, so processes that used the spawn/spill path clean up on
normal exit. A SIGKILLed process cannot run in-process teardown; the
machine-side timer remains the backstop for that path.
Agent Note: .agents/notes/implemented/process/2026-08-28-test-temp-dir-self-cleanup.md
- `JobStart` is a union discriminated by `record`: `RecordingJobStart.run`
receives `RecordingJob` (with `append`), `PlainJobStart.run` receives the
plain `RunningJob`, and the registry builds the plain face without the
method, so the former warn-and-drop path is unrepresentable.
- Remove the duplicated JSDoc opener in the registry seam and correct the
job-list slot comment (order 20 sits before the subagent catalog's 30).
- Restore the `cordis-tool-round` session fixture whose only change was a
re-recorded `createdAt`.
- Update the 2026-08-08 job-display note's stale service-disposal and slot
ordering facts; archive the superseded 2026-08-25 unified-task-list note
and point its remaining inbound links at the archived snapshot.
Review asked for the observation wire and its client service to leave
session-controller: a job should reach the Host only through its own
Remote namespace, and the session plugin's apply had to materialize
{ $stream, session } for a service that is not session state.
@deepseek-ai/dsh-api-job-controller now owns the Host JobController
(job.observe over ctx.jobs, requiring the agent and job registries) and
the client ClientJobOutput (ctx.jobOutput) over ClientJobOutputModel,
resolving both Remote faces in its own apply. The remotes assembly
mounts the job namespace; ui-jobs injects the new package; the wire
types drop their Session prefix and spell the status union out so the
client face pulls no Host declaration merges into browser programs.
session-controller keeps the jobsBySession roster and loses observe-job,
job-output, and the observe cadence config.
ui-chat's slots and snapshot-builder import CommandNode and friends from
the declaring package: the typert analyzer's cross-package rule trips on
the snapshot re-export as soon as another client face joins the root
Client program (the new controller does), which is why this branch
carried the change before. JobPumpSource documents why it is an offset
snapshot reader rather than a Node.js Readable.