Files
deepseek-harness/docs/persistence-catalog.md
T
Tianyi Cui b59d245c7c feat: Code Mode — the registry's mode config, the SDK codegen, and the run_code bridge
The dsh-tools half of the Code Mode RFC (its fourth, final change): the
registry gains its first config — mode: native | code | both — and OWNS how
its tools reach the model. 'code' contributes exactly one wire tool,
run_code, plus a lazy tools:sdk prompt section declaring every other tool
as a generated TypeScript API (jsonSchemaToTs: total over the defineTool
subset, unknown degradation, lexicographic byte-identical rendering);
'both' ships both representations; 'native' is byte-for-byte the old
behavior. Non-native modes fail every assembly loudly without a
typescript-language ctx.codeRuntime.

run_code's dispatch bridge: JSON-normalizes each binding argument before
dispatch (what dispatches is what the tool/code-dispatch event logs — the
append can never fail on payload shape; BigInt/circulars reject that one
call), serializes all program tool calls through a per-run queue (even
Promise.all — no concurrency-safety metadata yet), routes every sub-call
through tools/pre-execute → tools/post-execute (a deny rejects the
program-side promise), drops sub-call additionalContext (no safe outlet
mid-run; pinned), owns a run-scoped abort that follows the outer signal in
and fires on settlement (in-flight sub-dispatch aborted, queued abandoned,
queue drained before returning), and converts a failed run into
CodeRunFailedError → a structured isError carrying kind + captured logs.
tool/code-dispatch joins SessionEventMap by declaration merging (log-only;
deriveMessages ignores it).

The composed surface: the tools config forwards through agent-core and
both app packages; examples/code-agent + demo:code run the worker runtime
under mode code (keyless boot smoke + a with-key e2e proving the collapsed
[run_code] header, the dispatch events, and the file the program wrote);
two new snapshot scenarios (code-mode-turn, both-mode-turn) record the SDK
section, collapsed header, dispatch events, and result card — each its own
header-pinning class (the harness gains per-scenario config overlays and
per-class pins). Catalogs, graphs, cookbook, hooks-bridge notes, and the
RFC (moved to implemented/, restructured to decision-era headings) updated
in the same change.
2026-07-08 12:58:23 +08:00

15 KiB

Persistence Log Event Catalog

Every event type that can appear in a session's durable event log: each member of the merge-extensible SessionEventMap — the owning vocabulary in @deepseek-ai/dsh-session plus every plugin declaration merge in this repo — with the payload it carries, its surface badge, and the declaration it comes from. It complements session.md (the SessionEvent envelope, surface list, and deriveMessages() projection), persistence.md (how the log is made durable), and the cordis events catalog (the live bus wiring — a log event is NOT a cordis event; it reaches listeners via the single session/event emit).

This file is GENERATED from source (scripts/gen-persistence-catalog.ts) and verified fresh by pnpm run verify-persistence-catalog (part of doc-sync) — do not edit it by hand. Payload blocks use a ts persistence-catalog fence (skipped by doc-typecheck, since a bare payload fragment is not standalone-compilable). Type names in a payload link to the page that documents them. See the persistence-log-catalog RFC.

The on-disk envelope around every payload is SessionEventtype, monotonic seq, epoch-ms time, the data documented here, plus surfaceOp/sourceEventSeqs on surface events only (envelope). surface marks a SurfaceEventType member: it produces an LLM message and declares how it joins the surface list. log-only marks everything else: durable, replayable record with no derived-history contribution. Every payload is JSON-serializable (enforced at Session.append), and the whole format is pinned at SESSION_FORMAT_VERSION = 0 — pre-release, no compatibility implied (the version stance). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction.

Events

assistant/*

assistant/chunk — log-only

Raw stream chunk — token-level replay fidelity.

'assistant/chunk': { turn: number; step: number; chunk: StreamChunk }

Types: StreamChunk

Source: packages/core/session/src/types.ts:304

assistant/message — surface

Assembled assistant message for one step (derived history uses this). Carries the step's usage when the adapter reported token accounting, so the model output and its accounting travel together (there is no separate usage record). usage is absent when the adapter reported none.

'assistant/message': { turn: number; step: number; content: ContentBlock[]; usage?: TokenUsage }

Types: ContentBlock · TokenUsage

Source: packages/core/session/src/types.ts:311

compact/*

compact/end — log-only

Marks the end of a compaction — log-only, releases the lock. error set if summarization failed.

'compact/end': { turn: number; error?: string }

Source: packages/compact/compact/src/types.ts:46

compact/start — log-only

Marks the start of a compaction — log-only, holds the lock until compact/end.

'compact/start': { turn: number }

Source: packages/compact/compact/src/types.ts:23

compact/summary — log-only

Provenance record of a completed summarization — log-only, no surfaceOp. The summary content is in data.summary; the actual surface replacement is performed by a subsequent user/message event that shadows the compacted range.

'compact/summary': { summary: ContentBlock[]; shadowedRange: { start: number; end: number }; shadowedSeqs: number[]; shadowedTokenCount: number; model: string; maxTokens?: number }

Types: ContentBlock

Source: packages/compact/compact/src/types.ts:30

context/*

context/message — surface

In-session context injection (file-change notices, subdir AGENTS.md, skill content, cron notifications, …). Rendered into the derived history as tagged synthetic context — NOT a user prompt.

'context/message': { content: ContentBlock[]; source: MessageSource }

Types: ContentBlock · MessageSource

Source: packages/core/session/src/types.ts:302

hook/*

hook/invoked — log-only

A hook command was invoked at a hook point — log-only provenance (like compact/*; NOT a SurfaceEventType, carries no surfaceOp). dialect is the bridge that ran it (claude/codex), point the hook point (PreToolUse, Stop, …), matcher the matcher-group pattern that selected it (absent for match-all), handlerId a stable id for the command (so an invoked/result pair correlates). turn is the open turn the invocation lives inside.

'hook/invoked': { turn: number; point: string; dialect: HookDialect; matcher?: string; handlerId: string }

Source: packages/hooks/hook-protocol/src/types.ts:27

hook/result — log-only

A hook command's outcome — log-only, paired with a prior hook/invoked (same handlerId). decision is the dialect-neutral outcome derived by appendHookResult (which owns the rule): the hook's parsed decision (approve/allow/block/deny/ask), else 'stop' when it asked to halt via continue:false, else 'pass'. exitCode is the process exit (absent if it never ran), stderrSummary the trimmed stderr truncated to the bridge's configured cap (the block reason source on exit 2), durationMs the wall-clock runtime (audit timing; snapshot replay normalizes it). turn matches the hook/invoked.

'hook/result': { turn: number; point: string; handlerId: string; decision: string; exitCode?: number; stderrSummary?: string; durationMs: number }

Source: packages/hooks/hook-protocol/src/types.ts:45

prompt/*

prompt/blocked — log-only

A queued prompt an agent/prompt-submit listener VETOED — the durable record of a blocked prompt and why. Appended in place of the user/message the prompt would have become, so the block survives replay even in a MIXED batch where another queued prompt is allowed (there the turn does not end rejected, so the boundary reason alone would not preserve it). content is the original prompt the listener rejected; reason is the veto text (PromptDecision block.reason). NOT a SurfaceEventType: a blocked prompt produces no LLM message and never reaches deriveMessages().

'prompt/blocked': { content: ContentBlock[]; source: MessageSource; reason: string }

Types: ContentBlock · MessageSource

Source: packages/core/session/src/types.ts:296

request/*

request/header — log-only

Full snapshot of the EpochHeader the NEXT request is built under, with the RequestHeaderReason it was recorded whole. Appended by the loop inside the step, before dispatch, on a loop instance's first request-building step ('initial'/'resume') or when a delta failed its round-trip guard ('fallback'); always records what the request actually used, post-agent/request. Anchors the header fold: reconstruction reads the latest snapshot and applies the deltas after it. NOT a SurfaceEventType: it produces no LLM message — it is the request envelope, logged so every request is a pure function of the session log (the reconstructability RFC).

'request/header': { header: EpochHeader; reason: RequestHeaderReason }

Source: packages/core/session/src/types.ts:356

request/header-delta — log-only

Amendment to the folded EpochHeader: at least one of a SystemDelta, a ToolsDelta, or a whole replacement LlmCallConfig (four scalars — not worth diffing). Appended by the loop inside the step, before dispatch, when the header for this request differs from the fold of the log so far; the writer verifies applyHeaderDelta(previous, delta) reproduces the new header exactly and falls back to a 'fallback' request/header snapshot when it cannot, so a logged delta ALWAYS round-trips. NOT a SurfaceEventType.

'request/header-delta': { system?: SystemDelta; tools?: ToolsDelta; config?: LlmCallConfig }

Source: packages/core/session/src/types.ts:367

steering/*

steering/message — surface

Steering content injected between steps of a running turn.

'steering/message': { turn: number; content: ContentBlock[]; source: MessageSource }

Types: ContentBlock · MessageSource

Source: packages/core/session/src/types.ts:329

step/*

step/end — log-only

Closes step step of turn turn.

'step/end': { turn: number; step: number }

Source: packages/core/session/src/types.ts:283

step/start — log-only

Opens step step of turn turn — one model call plus the tool executions it requested.

'step/start': { turn: number; step: number }

Source: packages/core/session/src/types.ts:281

todo/*

todo/write — log-only

The agent's whole todo list, carried as a full snapshot and replaced wholesale on each write — the current list is the most recent todo/write (last-write-wins on replay, no fold). Appended by an owning agent via session.append('todo/write', { todos }).

NOT a SurfaceEventType: it produces no LLM message and never reaches deriveMessages(), so it carries no surfaceOp and stays off the surface — it is durable, replayable UI state, distinct from the conversation history. It is a SessionEventMap member riding the existing session/event emit, not a first-class Cordis interface Events notification, so it has no cordis-catalog row.

'todo/write': { todos: TodoItem[] }

Types: TodoItem

Source: packages/core/session/src/types.ts:343

tool/*

tool/call — log-only

The model requested one tool invocation: name with the raw arguments JSON string exactly as the model produced it (unparsed). callId pairs the call with its tool/result.

'tool/call': { turn: number; step: number; callId: CallId; name: string; arguments: string }

Types: CallId

Source: packages/core/session/src/types.ts:317

tool/code-dispatch — log-only

One bridged sub-dispatch from a run_code program: the parent run_code call id, the deterministic sub-call id (<parent>:code:<n>), the tool name with its JSON-normalized arguments — the exact value dispatched, normalized BEFORE dispatch, so this append can never fail on payload shape — whether the sub-call errored, and a bounded resultSummary of its model-facing text. Log-only: deriveMessages() ignores it, so sub-calls never re-enter model context; persistence and UIs get every call. Appended inside the parent run_code's execution (the bridge drains its queue before returning), so the turn-enclosure invariant holds by construction.

'tool/code-dispatch': { parentCallId: CallId; subCallId: CallId; name: string; arguments: unknown; isError: boolean; resultSummary: string }

Types: CallId

Source: packages/core/tools/src/code-mode.ts:36

tool/result — surface

A completed tool call's model-facing result, plus an optional tool-private meta presentation payload. meta is opaque to the core (unknown — the producing tool owns its shape and reads it back in presentResult) but MUST be JSON-serializable: Session.append runtime-validates all event data with isJsonValue, so a non-serializable meta is rejected at the source, and the durable log reproduces the identical card on replay. Absent unless the tool attaches one (e.g. dsh-tool-fs carries its result-time contextual diff here).

'tool/result': { turn: number; step: number; callId: CallId; content: ContentBlock[]; isError: boolean; error?: { name: string; code: string }; meta?: unknown }

Types: CallId · ContentBlock

Source: packages/core/session/src/types.ts:327

turn/*

turn/end — log-only

Closes turn turn with the TurnEndReason that ended it. The loop fires the awaited session/flush checkpoint at every turn end, so the turn boundary is also the durable-commit boundary.

'turn/end': { turn: number; reason: TurnEndReason }

Types: TurnEndReason

Source: packages/core/session/src/types.ts:279

turn/start — log-only

Opens turn turn. trigger records what started it — a drained message batch or an idle-time injection. The turn is the durability/replay boundary: every event sits between a turn/start and its matching turn/end (the turn-enclosure invariant).

'turn/start': { turn: number; trigger: TurnTrigger }

Types: TurnTrigger

Source: packages/core/session/src/types.ts:273

user/*

user/message — surface

A user-visible prompt (queued message drained at turn start).

'user/message': { content: ContentBlock[]; source: MessageSource }

Types: ContentBlock · MessageSource

Source: packages/core/session/src/types.ts:285