mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
A preset names a bundle of the two mechanism knobs — request = workspace-write + ask, yolo = danger-full-access + never — so the editor shows ONE 'Permissions' select where the sandbox-mode and approval-policy tiers stay orthogonal capabilities (the Codex /approvals shape: presets over two dials). ctx.permission (dsh-permission) owns the config-defined table, validates the default preset's bundle against the composed knob defaults at load (fails loud), and writes a switch THROUGH: one log-only permission/preset event (the audit fact reverse-mapping cannot recover — the planned 'agent' preset shares request's knob values and differs only in composed policy) plus each knob event via its own setter, deduped — a net-zero switch appends nothing. Every knob consumer keeps reading its own fold, untouched. The current preset DERIVES from the effective knob values — the fold breaks bundle ties, a knob state outside the table is the reserved 'custom' value (a state, not an error: shown while it holds, switchable FROM, never a target), and defaultPreset disappears (zero-event state reverse-maps from the composition defaults). The ACP bridge drops the two per-knob selects for the one preset select (advertised only when ctx.permission is composed); pending/anchor/no-op semantics carry over unchanged, with the no-op echo acknowledged before vocabulary validation so a client re-pushing a derived 'custom' current never errors. The sandbox variant example composes the service with a workspace-write default; the permission-switching, escalation-approved and escalation-rejected scenarios are re-recorded under it (escalations now target an outside-workspace /tmp path under danger-full-access, self-cleaning) and config-options is re-authored on the single-select wire.
333 lines
18 KiB
Markdown
333 lines
18 KiB
Markdown
<!-- Generated by scripts/gen-persistence-catalog.ts — do not edit by hand.
|
|
Run `pnpm run gen-persistence-catalog` to regenerate. -->
|
|
|
|
# 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](core-data-structures/session.md) (the `SessionEvent` envelope, surface list, and `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. 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](rfc/implemented/process/2026-07-04-persistence-log-catalog.md).
|
|
|
|
The on-disk envelope around every payload is `SessionEvent` — `type`, monotonic `seq`, epoch-ms `time`, the `data` documented here, plus `surfaceOp`/`sourceEventSeqs` on **surface** events only ([envelope](core-data-structures/session.md#sessioneventt--one-log-entry)). **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](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.
|
|
|
|
## 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).
|
|
|
|
```ts persistence-catalog
|
|
'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:84`](../packages/ui/user-approval/src/index.ts)
|
|
|
|
#### `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'`.
|
|
|
|
```ts persistence-catalog
|
|
'approval/decided': { id: ApprovalRequestId; outcome: ApprovalOutcome }
|
|
```
|
|
|
|
Source: [`packages/ui/user-approval/src/index.ts:95`](../packages/ui/user-approval/src/index.ts)
|
|
|
|
#### `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 (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).
|
|
|
|
```ts persistence-catalog
|
|
'approval/policy': { policy: ApprovalPolicy }
|
|
```
|
|
|
|
Source: [`packages/ui/user-approval/src/index.ts:107`](../packages/ui/user-approval/src/index.ts)
|
|
|
|
### `assistant/*`
|
|
|
|
#### `assistant/chunk` — log-only
|
|
|
|
Raw stream chunk — token-level replay fidelity.
|
|
|
|
```ts persistence-catalog
|
|
'assistant/chunk': { turn: number; step: number; chunk: StreamChunk }
|
|
```
|
|
|
|
Types: [StreamChunk](core-data-structures/llm-streaming.md)
|
|
|
|
Source: [`packages/core/session/src/types.ts:322`](../packages/core/session/src/types.ts)
|
|
|
|
#### `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.
|
|
|
|
```ts persistence-catalog
|
|
'assistant/message': { turn: number; step: number; content: ContentBlock[]; usage?: TokenUsage }
|
|
```
|
|
|
|
Types: [ContentBlock](core-data-structures/core.md) · [TokenUsage](core-data-structures/llm-streaming.md)
|
|
|
|
Source: [`packages/core/session/src/types.ts:329`](../packages/core/session/src/types.ts)
|
|
|
|
### `bash/*`
|
|
|
|
#### `bash/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 (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).
|
|
|
|
```ts persistence-catalog
|
|
'bash/sandbox-mode': { mode: SandboxMode }
|
|
```
|
|
|
|
Source: [`packages/bash/bash/src/session-mode.ts:31`](../packages/bash/bash/src/session-mode.ts)
|
|
|
|
### `compact/*`
|
|
|
|
#### `compact/end` — log-only
|
|
|
|
Marks the end of a compaction — log-only, releases the lock. `error` set if summarization failed.
|
|
|
|
```ts persistence-catalog
|
|
'compact/end': { turn: number; error?: string }
|
|
```
|
|
|
|
Source: [`packages/compact/compact/src/types.ts:46`](../packages/compact/compact/src/types.ts)
|
|
|
|
#### `compact/start` — log-only
|
|
|
|
Marks the start of a compaction — log-only, holds the lock until `compact/end`.
|
|
|
|
```ts persistence-catalog
|
|
'compact/start': { turn: number }
|
|
```
|
|
|
|
Source: [`packages/compact/compact/src/types.ts:23`](../packages/compact/compact/src/types.ts)
|
|
|
|
#### `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.
|
|
|
|
```ts persistence-catalog
|
|
'compact/summary': { summary: ContentBlock[]; shadowedRange: { start: number; end: number }; shadowedSeqs: number[]; shadowedTokenCount: number; model: string; maxTokens?: number }
|
|
```
|
|
|
|
Types: [ContentBlock](core-data-structures/core.md)
|
|
|
|
Source: [`packages/compact/compact/src/types.ts:30`](../packages/compact/compact/src/types.ts)
|
|
|
|
### `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.
|
|
|
|
```ts persistence-catalog
|
|
'context/message': { content: ContentBlock[]; source: MessageSource }
|
|
```
|
|
|
|
Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md)
|
|
|
|
Source: [`packages/core/session/src/types.ts:320`](../packages/core/session/src/types.ts)
|
|
|
|
### `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.
|
|
|
|
```ts persistence-catalog
|
|
'hook/invoked': { turn: number; point: string; dialect: HookDialect; matcher?: string; handlerId: string }
|
|
```
|
|
|
|
Source: [`packages/hooks/hook-protocol/src/types.ts:27`](../packages/hooks/hook-protocol/src/types.ts)
|
|
|
|
#### `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`.
|
|
|
|
```ts persistence-catalog
|
|
'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`](../packages/hooks/hook-protocol/src/types.ts)
|
|
|
|
### `permission/*`
|
|
|
|
#### `permission/preset` — log-only
|
|
|
|
The session's permission preset was switched — log-only (the `bash/sandbox-mode` precedent): durable and replayable, never in the model transcript. The LAST such event is the session's preset (effectivePermissionPreset); the knob events the switch wrote through follow it in the same turn, and they — not this record of the user's choice — are what execution reads.
|
|
|
|
```ts persistence-catalog
|
|
'permission/preset': { preset: string }
|
|
```
|
|
|
|
Source: [`packages/ui/permission/src/index.ts:42`](../packages/ui/permission/src/index.ts)
|
|
|
|
### `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()`.
|
|
|
|
```ts persistence-catalog
|
|
'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:314`](../packages/core/session/src/types.ts)
|
|
|
|
### `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).
|
|
|
|
```ts persistence-catalog
|
|
'request/header': { header: EpochHeader; reason: RequestHeaderReason }
|
|
```
|
|
|
|
Source: [`packages/core/session/src/types.ts:374`](../packages/core/session/src/types.ts)
|
|
|
|
#### `request/header-delta` — log-only
|
|
|
|
Amendment to the folded EpochHeader: at least one of a SystemDelta, a ToolsDelta, a whole replacement LlmCallConfig (four scalars — not worth diffing), or a whole replacement session prefix (`messagePrefix` — small advisory content, replaced whole; an EMPTY array encodes the transition to "none", mirroring the canonical form's absent field — the loop never produces one in practice: the prefix is composed once per instance and anchored by that instance's snapshot, so this arm exists for codec totality). 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.
|
|
|
|
```ts persistence-catalog
|
|
'request/header-delta': { system?: SystemDelta; tools?: ToolsDelta; config?: LlmCallConfig; messagePrefix?: Message[] }
|
|
```
|
|
|
|
Source: [`packages/core/session/src/types.ts:391`](../packages/core/session/src/types.ts)
|
|
|
|
### `steering/*`
|
|
|
|
#### `steering/message` — surface
|
|
|
|
Steering content injected between steps of a running turn.
|
|
|
|
```ts persistence-catalog
|
|
'steering/message': { turn: number; content: ContentBlock[]; source: MessageSource }
|
|
```
|
|
|
|
Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md)
|
|
|
|
Source: [`packages/core/session/src/types.ts:347`](../packages/core/session/src/types.ts)
|
|
|
|
### `step/*`
|
|
|
|
#### `step/end` — log-only
|
|
|
|
Closes step `step` of turn `turn`.
|
|
|
|
```ts persistence-catalog
|
|
'step/end': { turn: number; step: number }
|
|
```
|
|
|
|
Source: [`packages/core/session/src/types.ts:301`](../packages/core/session/src/types.ts)
|
|
|
|
#### `step/start` — log-only
|
|
|
|
Opens step `step` of turn `turn` — one model call plus the tool executions it requested.
|
|
|
|
```ts persistence-catalog
|
|
'step/start': { turn: number; step: number }
|
|
```
|
|
|
|
Source: [`packages/core/session/src/types.ts:299`](../packages/core/session/src/types.ts)
|
|
|
|
### `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.
|
|
|
|
```ts persistence-catalog
|
|
'todo/write': { todos: TodoItem[] }
|
|
```
|
|
|
|
Types: [TodoItem](core-data-structures/session.md)
|
|
|
|
Source: [`packages/core/session/src/types.ts:361`](../packages/core/session/src/types.ts)
|
|
|
|
### `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`.
|
|
|
|
```ts persistence-catalog
|
|
'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:335`](../packages/core/session/src/types.ts)
|
|
|
|
#### `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.
|
|
|
|
```ts persistence-catalog
|
|
'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:38`](../packages/core/tools/src/code-mode.ts)
|
|
|
|
#### `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).
|
|
|
|
```ts persistence-catalog
|
|
'tool/result': { turn: number; step: number; callId: CallId; content: ContentBlock[]; isError: boolean; error?: { name: string; code: string }; meta?: unknown }
|
|
```
|
|
|
|
Types: [CallId](core-data-structures/core.md) · [ContentBlock](core-data-structures/core.md)
|
|
|
|
Source: [`packages/core/session/src/types.ts:345`](../packages/core/session/src/types.ts)
|
|
|
|
### `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.
|
|
|
|
```ts persistence-catalog
|
|
'turn/end': { turn: number; reason: TurnEndReason }
|
|
```
|
|
|
|
Types: [TurnEndReason](core-data-structures/session.md)
|
|
|
|
Source: [`packages/core/session/src/types.ts:297`](../packages/core/session/src/types.ts)
|
|
|
|
#### `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).
|
|
|
|
```ts persistence-catalog
|
|
'turn/start': { turn: number; trigger: TurnTrigger }
|
|
```
|
|
|
|
Types: [TurnTrigger](core-data-structures/session.md)
|
|
|
|
Source: [`packages/core/session/src/types.ts:291`](../packages/core/session/src/types.ts)
|
|
|
|
### `user/*`
|
|
|
|
#### `user/message` — surface
|
|
|
|
A user-visible prompt (queued message drained at turn start).
|
|
|
|
```ts persistence-catalog
|
|
'user/message': { content: ContentBlock[]; source: MessageSource }
|
|
```
|
|
|
|
Types: [ContentBlock](core-data-structures/core.md) · [MessageSource](core-data-structures/core.md)
|
|
|
|
Source: [`packages/core/session/src/types.ts:303`](../packages/core/session/src/types.ts)
|