# Session Persistence Event Catalog Every event type that can appear in a session's durable event log: the complete persisted `SessionEvent` envelope and each member of the merge-extensible `SessionEventMap` — the owning vocabulary in `@deepseek-ai/dsh-session` plus every plugin declaration merge in this repo — with source JSDoc, full payload declaration, surface badge, and declaration site. It complements [session.md](core-data-structures/session.md) (surface ordering and the `deriveMessages()` projection), [persistence.md](core-data-structures/persistence.md) (how the log is made durable), and the [cordis events catalog](cordis-catalog/events.md) (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. Declaration blocks retain the source declaration and nested property JSDoc, removing only the indentation imposed by a containing interface/module, and use a `ts persistence-catalog` fence (skipped by doc-typecheck because declarations reference types from their owning modules). Type names in a payload link to the page that documents them. See [the persistence-log-catalog Agent Note](../.agents/notes/implemented/process/2026-07-04-persistence-log-catalog.md). The envelope declarations below compose each event's `type`, monotonic `seq`, epoch-ms `time`, `data`, and the conditional `surfaceOp`/`sourceEventSeqs` fields. **surface** marks a `SurfaceEventType` member: it produces an LLM message and declares how it joins the surface list. **log-only** marks everything else: a 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](core-data-structures/persistence.md)). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction. ## Event envelope ```ts persistence-catalog /** The appendable event-type keys of {@link SessionEventMap}, plugin-merged extensions included. */ export type SessionEventType = keyof SessionEventMap /** * The subset of {@link SessionEventType} values whose events produce LLM * messages and are eligible to appear on the ordered surface. Only these * event types may carry {@link SurfaceOp} and {@link SessionEvent.sourceEventSeqs}. */ export type SurfaceEventType = | 'user/message' | 'assistant/message' | 'tool/result' | 'steering/message' /** * How a session event entered the ordered surface. Only valid on * {@link SurfaceEventType} events. * * - `'append'`: added to the tail — normal path for user/assistant/tool/steering * messages. * - `{ op: 'replace', start, end }`: replaces surface nodes from `start` * (inclusive) through `end` (inclusive) with this node. Both must exist as * surface nodes in the current surface. `start === end` replaces a single * node. The node's {@link SessionEvent.sourceEventSeqs} must include every * shadowed surface node. Used by compaction and possible other manipulations. */ export type SurfaceOp = | 'append' | { op: 'replace'; start: number; end: number } /** * One immutable entry in the session log. * * A proper discriminated union over `type` (not independent `type`/`data` * unions), so `switch (event.type)` narrows `event.data` without casts. * * The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional: * they only exist on {@link SurfaceEventType} variants (`user/message`, * `assistant/message`, `tool/result`, `steering/message`). * Non-surface events (boundary markers, chunks, usage, errors) never carry * surface metadata — the compiler enforces this at `Session.append()` * call sites. */ export type SessionEvent = { [K in SessionEventType]: { type: K /** Monotonic sequence number within the session. */ seq: number /** Unix epoch milliseconds. */ time: number data: SessionEventMap[K] } & (K extends SurfaceEventType ? { /** * Seq numbers of events that are provenance sources of this event * (e.g. the `assistant/chunk` seqs that built an `assistant/message`, * or the surface nodes shadowed by a compaction replace node). An * `assistant/message` may carry a present empty array for a known empty * provider stream; omission means unrecorded provenance. */ sourceEventSeqs?: number[] /** How this event entered the surface; absent for non-surface events. */ surfaceOp?: SurfaceOp } : object) }[T] ``` Sources: [`packages/core/session/src/types.ts:324`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:337`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:366`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:398`](../packages/core/session/src/types.ts) ## Events ### `approval/*` #### `approval/asked` — log-only ```ts persistence-catalog /** * An approval question was put to the answerer chain — log-only audit * (like `hook/*`; NOT a surface event, carries no `surfaceOp`). `id` pairs * it with the `approval/decided` that always follows; `toolName` is the * tool the question is about, `callId` the exact tool call when the asker * had one, `reason` the asker's human-readable explanation (e.g. a hook's * permission-decision reason). */ 'approval/asked': { id: ApprovalRequestId toolName: string callId?: CallId reason?: string } ``` Types: [CallId](core-data-structures/core.md) Source: [`packages/ui/user-approval/src/index.ts:44`](../packages/ui/user-approval/src/index.ts) #### `approval/decided` — log-only ```ts persistence-catalog /** * The outcome of a prior `approval/asked` (same `id`) — log-only audit. * Exactly one per ask, appended when the outcome is known: a decision, a * cancellation, or the fail-closed `'unavailable'`. */ 'approval/decided': { id: ApprovalRequestId outcome: ApprovalOutcome } ``` Source: [`packages/ui/user-approval/src/index.ts:55`](../packages/ui/user-approval/src/index.ts) #### `approval/policy` — log-only ```ts persistence-catalog /** * The session's approval policy was switched — log-only, durable, * replayable, never in the model transcript (the model learns the policy * from the prompt section and the narrator's notices). The LAST such * event is the session's override ({@link effectiveApprovalPolicy}); * who asked for it is derivable from position (an event after the log's * last `request/header` was a runtime switch by the user). */ 'approval/policy': { policy: ApprovalPolicy } ``` Source: [`packages/ui/user-approval/src/index.ts:67`](../packages/ui/user-approval/src/index.ts) ### `assistant/*` #### `assistant/chunk` — log-only ```ts persistence-catalog /** Raw stream chunk — token-level replay fidelity. */ 'assistant/chunk': { turn: number; step: number; chunk: StreamChunk } ``` Types: [StreamChunk](core-data-structures/llm-streaming.md) Source: [`packages/core/session/src/types.ts:270`](../packages/core/session/src/types.ts) #### `assistant/message` — surface ```ts persistence-catalog /** * 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[]; provenance: AssistantProvenance; usage?: TokenUsage } ``` Types: [ContentBlock](core-data-structures/core.md) · [TokenUsage](core-data-structures/llm-streaming.md) Source: [`packages/core/session/src/types.ts:277`](../packages/core/session/src/types.ts) ### `compact/*` #### `compact/end` — log-only ```ts persistence-catalog /** 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:40`](../packages/compact/compact/src/types.ts) #### `compact/start` — log-only ```ts persistence-catalog /** 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:15`](../packages/compact/compact/src/types.ts) #### `compact/summary` — log-only ```ts persistence-catalog /** * 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 /** The provider route that wrote the summary. */ provider: string /** * The model that wrote the summary — the summarize call's envelope, * reported by the backend that made the call, logged so the one-shot * request is reconstructable from log + code and "which model wrote * this summary" has a durable answer (the reconstructability Agent Note). */ model: string /** The generation cap the summarize call sent, when one applied. */ maxTokens?: number } ``` Types: [ContentBlock](core-data-structures/core.md) Source: [`packages/compact/compact/src/types.ts:22`](../packages/compact/compact/src/types.ts) ### `hook/*` #### `hook/invoked` — log-only ```ts persistence-catalog /** * A hook command was invoked at a hook point — log-only provenance (like * `compact/*`; NOT a {@link 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:19`](../packages/hooks/hook-protocol/src/types.ts) #### `hook/result` — log-only ```ts persistence-catalog /** * Log-only outcome paired to `hook/invoked` by `handlerId`. Decision is the * parsed permission result, `stop` for `continue:false`, or `pass`; exit code * may be absent, stderr is bounded, and duration is wall-clock runtime. */ 'hook/result': { turn: number point: string handlerId: string decision: string exitCode?: number stderrSummary?: string durationMs: number } ``` Source: [`packages/hooks/hook-protocol/src/types.ts:31`](../packages/hooks/hook-protocol/src/types.ts) ### `llm/*` #### `llm/retry` — log-only ```ts persistence-catalog /** Durable, non-surface record of one transient retry scheduled after a closed failed step. */ 'llm/retry': { turn: number step: number retry: number maxRetries: number delayMs: number failure: LlmFailure } ``` Source: [`packages/llm/llm-retry/src/index.ts:18`](../packages/llm/llm-retry/src/index.ts) ### `permission/*` #### `permission/preset` — log-only ```ts persistence-catalog /** * Records the selected preset as durable, log-only user intent. The knob * events follow in the same turn and control execution; this event stays * out of the model transcript and lets {@link effectivePermissionPreset} * preserve a selection when bundles match. */ 'permission/preset': { preset: string } ``` Source: [`packages/ui/permission/src/index.ts:36`](../packages/ui/permission/src/index.ts) ### `plan/*` #### `plan/mode` — log-only ```ts persistence-catalog /** * Whether plan mode is in force from this point on: log-only, non-surface, * whole-value replace. The last `plan/mode` wins; a log with none folds to * inactive through {@link foldPlanMode}. */ 'plan/mode': { active: boolean } ``` Source: [`packages/plan/plan-mode/src/index.ts:40`](../packages/plan/plan-mode/src/index.ts) ### `prompt/*` #### `prompt/blocked` — log-only ```ts persistence-catalog /** * Durable record of a prompt veto and its reason. It is log-only: the blocked * prompt never enters the model-visible surface, and its turn runs zero steps. */ 'prompt/blocked': { content: ContentBlock[]; source: MessageSource; reason: string } ``` Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md) Source: [`packages/core/session/src/types.ts:268`](../packages/core/session/src/types.ts) ### `request/*` #### `request/header` — log-only ```ts persistence-catalog /** * Full header for the next request, appended inside its step before dispatch. * It is log-only; the latest snapshot reconstructs the request header. */ 'request/header': { header: EpochHeader; reason: RequestHeaderReason } ``` Source: [`packages/core/session/src/types.ts:312`](../packages/core/session/src/types.ts) ### `sandbox/*` #### `sandbox/mode` — log-only ```ts persistence-catalog /** * The session's sandbox mode was switched — log-only (like `approval/*`; * NOT a surface event, carries no `surfaceOp`): durable and replayable, * never in the model transcript. The LAST such event is the session's * override ({@link effectiveSandboxMode}); who asked for it is derivable * from position (an event after the log's last `request/header*` was a * runtime switch by the user; see the tool layer's narrator). */ 'sandbox/mode': { mode: SandboxMode } ``` Source: [`packages/sandbox/sandbox-policy/src/session-mode.ts:34`](../packages/sandbox/sandbox-policy/src/session-mode.ts) ### `session/*` #### `session/title` — log-only ```ts persistence-catalog /** * Latest-wins session title snapshot. Log-only: it never enters the model * surface or derived history. */ 'session/title': SessionTitleEventData ``` Types: [SessionTitleEventData](core-data-structures/session-title.md) Source: [`packages/session-title/session-title/src/index.ts:96`](../packages/session-title/session-title/src/index.ts) #### `session/title-llm-request` — log-only ```ts persistence-catalog /** Log-only pre-dispatch record of one session-title model request. */ 'session/title-llm-request': SessionTitleLlmRequestEventData ``` Types: [SessionTitleLlmRequestEventData](core-data-structures/session-title.md) Source: [`packages/session-title/session-title-llm/src/index.ts:44`](../packages/session-title/session-title-llm/src/index.ts) ### `steering/*` #### `steering/message` — surface ```ts persistence-catalog /** Steering content injected between steps of a running turn. */ 'steering/message': PromptMessageData & { turn: number } ``` Source: [`packages/core/session/src/types.ts:305`](../packages/core/session/src/types.ts) ### `step/*` #### `step/end` — log-only ```ts persistence-catalog /** Closes step `step` of turn `turn`. */ 'step/end': { turn: number; step: number } ``` Source: [`packages/core/session/src/types.ts:253`](../packages/core/session/src/types.ts) #### `step/start` — log-only ```ts persistence-catalog /** 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:251`](../packages/core/session/src/types.ts) ### `todo/*` #### `todo/write` — log-only ```ts persistence-catalog /** Whole-list snapshot; latest write wins on replay. Log-only UI state; never derived history. */ 'todo/write': { todos: TodoItem[] } ``` Types: [TodoItem](core-data-structures/session.md) Source: [`packages/core/session/src/types.ts:307`](../packages/core/session/src/types.ts) ### `tool/*` #### `tool/call` — log-only ```ts persistence-catalog /** * 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](core-data-structures/core.md) Source: [`packages/core/session/src/types.ts:283`](../packages/core/session/src/types.ts) #### `tool/code-dispatch` — log-only ```ts persistence-catalog /** * One bridged sub-dispatch from a `run_code` program: the parent * `run_code` call id, the deterministic sub-call id * (`:code:`), 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. Before * bounding, occurrences of a non-root session workspace path are * normalized to `.` so host-specific absolute path lengths cannot change * the summary. * 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](core-data-structures/core.md) Source: [`packages/core/tools/src/code-mode.ts:34`](../packages/core/tools/src/code-mode.ts) #### `tool/result` — surface ```ts persistence-catalog /** * A completed tool call's model-facing result, optional internal failure * identity, and optional tool-private `meta` presentation payload. `meta` is * opaque to the core (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?: JsonValue } ``` Types: [CallId](core-data-structures/core.md) · [ContentBlock](core-data-structures/core.md) Source: [`packages/core/session/src/types.ts:295`](../packages/core/session/src/types.ts) ### `turn/*` #### `turn/end` — log-only ```ts persistence-catalog /** * Closes turn `turn` with the {@link TurnEndReason} that ended it. The loop * awaits `session/flush` after an ordinary turn ends before claiming the next * queued item. Success commits the turn; rejection is reported live and does * not prevent later work. */ 'turn/end': { turn: number; reason: TurnEndReason } ``` Types: [TurnEndReason](core-data-structures/session.md) Source: [`packages/core/session/src/types.ts:249`](../packages/core/session/src/types.ts) #### `turn/start` — log-only ```ts persistence-catalog /** * Opens turn `turn`. `trigger` records what started it — one claimed queued * message 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](core-data-structures/session.md) Source: [`packages/core/session/src/types.ts:242`](../packages/core/session/src/types.ts) ### `user/*` #### `user/message` — surface ```ts persistence-catalog /** * A user-role message on the model-visible surface: a direct human prompt * (the queued message claimed for this turn), a synthetic `agent.inject()` * context (file-change notices, subdir AGENTS.md, skill content, cron * notifications, …), or an admitted goal continuation round. All three * project their `content` verbatim; `source` (with a non-`user` kind marking * injected context) is the only channel that tells them apart. An idle * injection wraps this event in a one-shot turn so the log stays turn-enclosed. */ 'user/message': PromptMessageData ``` Source: [`packages/core/session/src/types.ts:263`](../packages/core/session/src/types.ts)