`SESSION_FORMAT_VERSION` is the only value that names the Session format
generation. Two other version fields had moved to 2 alongside it:
- The `dsh_session_log` request extension's outer schema `version` returns
to 1 and its Session header projection keeps `seedLength`, derived from
the logical inherited cut, so the external wire is unchanged in this PR;
`sessionFormatVersion` alone identifies the embedded generation.
- The `sessionStats` projection unit's `stateVersion` returns to 1: the
projection cache binds every checkpoint to the format generation, so a
generation change discards old rows without a unit version bump.
Test doubles and the standalone Client fixture that build a current
header now spell `SESSION_FORMAT_VERSION` instead of the literal.
The `start` frame and the reconnect baseline attempt carried a wall-clock
`startedTime` that no Host or Client consumer read: the Host accumulator
and the Client reconciler copied it into their attempt maps and nothing
looked at it again. Presentation frames now carry only the coordinates
consumers use (attempt, revision, turn, step, chunk index, and v1 seq
provenance); the type-equivalence block, event graph, READMEs, and the
live-stream Agent Note follow the type.
The header list watches its session's roster through ctx.jobs.watchRows
while mounted and reads rows and observed views from one jobs snapshot. A
row is expandable while the job is live or once it left retained output
behind; it shows the live progress line or the terminal detail. The web
e2e drivers kill through visibleTo(), and the client fixtures the roster
removal missed drop their jobs baselines.
The control baseline and frames carry queues and projections only; the
SessionJob row type, the jobs frame, the registry subscription, and the
client jobsBySession mirror are gone, and every client fixture drops the
empty mirror. The roster moves to dsh-api-job-controller's job.rows
stream in the next change.
Review corrections on the observation record:
- `pumpJobOutput` subscribes to `done` once and keeps one pending timer;
racing the same pending promise every poll round retained a reaction
per round for the life of the job.
- The `ctx.jobOutput` effect disposer is async and awaits every open
carrier's `dispose()`, so the fiber reports unloaded only once the old
iterators are quiescent.
- `SessionJob.record: true` replaces `outputTotal`: `append` never commits
a roster change, so the mirrored count was stale while output flowed;
the live offsets ride the observation stream's `opened` anchor.
- The record is documented as a best-effort live preview whose
cross-stream reordering is bounded by the poll interval.
- The web e2e scenarios hold their background job on a test-owned barrier
file instead of a wall-clock sleep, and each is one test rather than a
chain sharing a job id.
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.
The persistence seam is now create/open/stat/list returning per-session
SessionHandles (read/append/flush/close); every log read and write flows
through the owning handle. The seam package exports only the service and
handle contracts, consumer-visible errors, and pure durable-data
validation helpers; each backend owns its complete storage runtime, and
the shared contract suites pin equivalent observable behavior. The
backend routes published sessions' live events by id into the active
write handle; agent-loop only acquires, seeds, and closes the handle.
Resume appends interruptedTurnClosers through its write handle;
session-query owns the revision-keyed cold cache. Legacy-only surfaces
are removed in the same swap: locate/readRaw/supportsRawArtifacts, the
legacy event-shape read migration, zstd torn-frame salvage,
DSH_SESSION_JSONL, and hook transcript_path population; a torn final
zstd frame is discarded whole; the session-list cold blank probe returns
on stat metadata (eventCount derived from the last physical row,
sizeBytes). The WebUI ZIP export serializes the logical log from a read
handle, so both backends export identically.
Refs #3245
Absorbs the session seq-brand refactor and the turn-rail preview layer;
regenerates the catalogs and graph docs from the merged source and
re-ports the zh config/event rows this branch owns.
Covers the woken-wait fast path (a wake landing while the generator is
suspended on a yield), the opened frame's live-detail arm, and the
sessionId-less caller resolution over an unowned job.
Absorbs the repo-wide no-op invariant companion removal (ui-jobs loses its
placeholder companion, export, and devDep), the 0.1.2-alpha.3 release bump,
and the tool-subagent-report package retirement; regenerates the client slot
catalog for the shifted contract line numbers.
Observation (re)opens run on caller stacks — a React expand click, a
carrier retry — whose dynamic context has not declared remote.session, so
the ClientJobOutput wiring resolves the namespace to a concrete value while
the plugin's own context is current, exactly as the activity client did.
Caught by the live-job-stream web e2e, whose goldens (with the
background-job-list pair) now carry the ui-jobs list name; the client-apply
spec gains a jobOutput wiring smoke.
The regenerated session namespace requires the new stream; the fake serves
a never-yielding generator, and job-output.client.spec exercises the real
consumer.
SessionJob rows carry outputTotal exactly for record-declaring jobs, and a
new session.observeJob stream serves one job's retained record from an
absolute offset (anchor, coalesced output frames, terminal status) with the
fenced read resolved from the request's session — the same authorization
shape as the jobs roster. The client half gains ctx.jobOutput: the
reference-counted per-job observation service and its accumulated view
model, ported from the activity controller this replaces.
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.
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.