Files
deepseek-harness/docs/persistence-catalog.md
Tianyi Cui 06556237f4 fix(tools): close scheduler coverage gaps; regen persistence catalog
The coverage gate flagged three untaken paths in the bridge scheduler:
- the exclusive-head inFlight re-check was dead (the shared guard above
  already returns for an exclusive head with any in-flight sibling) — removed;
- the commit-cursor undefined-dispatched break was structurally unreachable
  once entries join commitQueue only after start() ran synchronously —
  reordered the pump so the invariant holds by construction, annotated;
- the finish (final-result) commit arm and the pump re-entry guard gain a
  covering test (throwing tools/pre-execute listener) and a defensive
  annotation respectively; mid-run unregistration test renamed to match its
  actual post-result settlement path.

Also covers the direct-construction maxParallelSubCalls default (index.ts)
and commits the regenerated persistence catalog for the new dispatch pair.
2026-07-26 13:24:34 +08:00

21 KiB

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 (surface ordering and the 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. 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.

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). Scope: the packages in this repo; a downstream plugin can merge further event types, which are outside this catalog by construction.

Event envelope

/** 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<T extends SessionEventType = SessionEventType> = {
  [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:337 · packages/core/session/src/types.ts:366 · packages/core/session/src/types.ts:398

Events

approval/*

approval/asked — log-only

/**
 * 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

Source: packages/ui/user-approval/src/index.ts:44

approval/decided — log-only

/**
 * 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

approval/policy — log-only

/**
 * 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

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:270

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[]; provenance: AssistantProvenance; usage?: TokenUsage }

Types: ContentBlock · TokenUsage

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

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:40

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:15

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
  /** 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

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

hook/*

hook/invoked — log-only

/**
 * 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

hook/result — log-only

/**
 * 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

llm/*

llm/retry — log-only

/** 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

permission/*

permission/preset — log-only

/**
 * 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

plan/*

plan/mode — log-only

/**
 * 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:41

prompt/*

prompt/blocked — log-only

/**
 * 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 · MessageSource

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

request/*

request/header — log-only

/**
 * 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

sandbox/*

sandbox/mode — log-only

/**
 * 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

session/*

session/title — log-only

/**
 * Latest-wins session title snapshot. Log-only: it never enters the model
 * surface or derived history.
 */
'session/title': SessionTitleEventData

Types: SessionTitleEventData

Source: packages/session-title/session-title/src/index.ts:96

session/title-llm-request — log-only

/** Log-only pre-dispatch record of one session-title model request. */
'session/title-llm-request': SessionTitleLlmRequestEventData

Types: SessionTitleLlmRequestEventData

Source: packages/session-title/session-title-llm/src/index.ts:44

steering/*

steering/message — surface

/** Steering content injected between steps of a running turn. */
'steering/message': PromptMessageData & { turn: number }

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

step/*

step/end — log-only

/** Closes step `step` of turn `turn`. */
'step/end': { turn: number; step: number }

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

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:251

todo/*

todo/write — log-only

/** Whole-list snapshot; latest write wins on replay. Log-only UI state; never derived history. */
'todo/write': { todos: TodoItem[] }

Types: TodoItem

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

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:283

tool/code-dispatch — log-only

/**
 * One bridged sub-dispatch SETTLING: the pairing ids (matching the
 * `tool/code-dispatch-start` with the same `subCallId`), the tool `name`
 * with the same JSON-normalized `arguments`, and the sub-call's complete
 * model-facing outcome in `tool/result`'s own vocabulary
 * (`content` + `isError`), so UIs render a sub-call through the exact
 * code path that renders a native call. Every started sub-call settles
 * with exactly one of these (abort included: the aborted pipeline result
 * is an `isError` outcome).
 * 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 in-flight dispatches
 * before returning), so the turn-enclosure invariant holds by
 * construction.
 */
'tool/code-dispatch': { parentCallId: CallId; subCallId: CallId; name: string; arguments: unknown; isError: boolean; content: ContentBlock[] }

Types: CallId · ContentBlock

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

tool/code-dispatch-start — log-only

/**
 * One sub-dispatch STARTING inside a `run_code` program: the parent
 * `run_code` call id, the deterministic sub-call id (`<parent>:code:<n>`,
 * numbered in submission order), and the tool `name` with its
 * JSON-normalized `arguments` — the exact value dispatched, normalized
 * BEFORE dispatch, so this append can never fail on payload shape.
 * Appended when the scheduler actually starts the call (not at
 * submission), so a start means the tool body pipeline was entered; a
 * call abandoned in the queue logs nothing. Log-only: `deriveMessages()`
 * ignores it; UIs use it for live per-sub-call running state and pair it
 * with `tool/code-dispatch` by `subCallId` (timing = the two events'
 * `time` fields).
 */
'tool/code-dispatch-start': { parentCallId: CallId; subCallId: CallId; name: string; arguments: unknown }

Types: CallId

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

tool/result — surface

/**
 * 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 · ContentBlock

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

turn/*

turn/end — log-only

/**
 * 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

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

turn/start — log-only

/**
 * 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

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

user/*

user/message — surface

/**
 * 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