@deepseek-ai/dsh-session 0.2.2-alpha.3 → 0.2.2-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.i18n.yaml +4 -4
  2. package/README.md +3 -1
  3. package/README.zh.md +3 -1
  4. package/lib/index.js +151 -79
  5. package/lib/types/fork.d.ts.map +1 -0
  6. package/lib/types/fork.js.map +1 -0
  7. package/lib/types/index.d.ts +17 -1
  8. package/lib/types/index.d.ts.map +1 -0
  9. package/lib/types/index.js +82 -63
  10. package/lib/types/index.js.map +1 -0
  11. package/lib/types/known-event-types.d.ts.map +1 -0
  12. package/lib/types/known-event-types.js +1 -0
  13. package/lib/types/known-event-types.js.map +1 -0
  14. package/lib/types/preparation.d.ts.map +1 -0
  15. package/lib/types/preparation.js.map +1 -0
  16. package/lib/types/repair.d.ts.map +1 -0
  17. package/lib/types/repair.js.map +1 -0
  18. package/lib/types/request-header.d.ts.map +1 -0
  19. package/lib/types/request-header.js.map +1 -0
  20. package/lib/types/seq-ranges.d.ts.map +1 -0
  21. package/lib/types/seq-ranges.js.map +1 -0
  22. package/lib/types/surface.d.ts +6 -0
  23. package/lib/types/surface.d.ts.map +1 -0
  24. package/lib/types/surface.js +57 -11
  25. package/lib/types/surface.js.map +1 -0
  26. package/lib/types/tool-history.d.ts.map +1 -0
  27. package/lib/types/tool-history.js.map +1 -0
  28. package/lib/types/types.d.ts +4 -0
  29. package/lib/types/types.d.ts.map +1 -0
  30. package/lib/types/types.js.map +1 -0
  31. package/package.json +14 -11
  32. package/src/fork.ts +30 -0
  33. package/src/index.ts +1555 -0
  34. package/src/known-event-types.ts +91 -0
  35. package/src/preparation.ts +49 -0
  36. package/src/repair.ts +211 -0
  37. package/src/request-header.ts +69 -0
  38. package/src/seq-ranges.ts +73 -0
  39. package/src/surface.ts +758 -0
  40. package/src/tool-history.ts +75 -0
  41. package/src/types.ts +574 -0
@@ -0,0 +1,75 @@
1
+ /** Stateful reconstruction of historical tool definitions for request projection. */
2
+
3
+ import type { ToolHistory, ToolSchema } from '@deepseek-ai/dsh-llm'
4
+ import { deepFreeze } from '@deepseek-ai/dsh-util-values'
5
+ import type { SessionEvent, SessionSeq } from './types.ts'
6
+
7
+ /** Folds committed headers and developer messages independently of model capability. */
8
+ export class ToolHistoryProjection {
9
+ /** Historical declarations indexed by the header sequence referenced by additions. */
10
+ private readonly headers = new Map<SessionSeq, readonly ToolSchema[]>()
11
+ /** Definitions retained in the current declaration series, including removed tools. */
12
+ private declared = new Map<string, ToolSchema>()
13
+ /** Active definitions from the latest request header. */
14
+ private active: readonly ToolSchema[] = []
15
+ /** Active names reconstructed from the baseline and recorded updates. */
16
+ private available = new Set<string>()
17
+ /** Header that starts the current declaration series; absent before the first header. */
18
+ private baselineSeq: SessionSeq | undefined
19
+ /** Immutable request snapshot of the current baseline and resolved updates. */
20
+ private history: ToolHistory = deepFreeze({ tools: [], updates: [] })
21
+
22
+ /**
23
+ * Consume the next committed event in log order.
24
+ * @param event - a session event, including inherited events during restoration.
25
+ */
26
+ apply(event: SessionEvent): void {
27
+ if (event.type === 'request/header') {
28
+ const tools = event.data.header.tools ?? []
29
+ this.headers.set(event.seq, tools)
30
+ // A retained name cannot acquire a different definition without losing
31
+ // prefix reuse; an unchanged restored name is re-offered by its recorded addition.
32
+ const redeclared = tools.some((tool) => {
33
+ const before = this.declared.get(tool.name)
34
+ return before !== undefined && JSON.stringify(before) !== JSON.stringify(tool)
35
+ })
36
+ if (this.baselineSeq === undefined || event.data.reason === 'series' || event.data.startsSeries || redeclared) {
37
+ this.baselineSeq = event.seq
38
+ this.declared = new Map(tools.map(tool => [tool.name, tool]))
39
+ this.history = deepFreeze({ tools, updates: [] })
40
+ this.available = new Set(tools.map(tool => tool.name))
41
+ }
42
+ this.active = tools
43
+ } else if (event.type === 'developer/message') {
44
+ const { message, headerSeq } = event.data
45
+ const definitions = headerSeq === undefined ? [] : this.headers.get(headerSeq)
46
+ const additions = message.content.flatMap((block) => {
47
+ if (block.type !== 'tool-addition') return []
48
+ const tool = definitions?.find(tool => tool.name === block.toolName)
49
+ if (tool === undefined) throw new Error(`tool history: missing definition for ${block.toolName}`)
50
+ return [tool]
51
+ })
52
+ for (const tool of additions) this.declared.set(tool.name, tool)
53
+ for (const block of message.content) {
54
+ if (block.type === 'tool-addition') this.available.add(block.toolName)
55
+ else if (block.type === 'tool-removal') this.available.delete(block.toolName)
56
+ }
57
+ this.history = deepFreeze({
58
+ tools: this.history.tools,
59
+ updates: [...this.history.updates, { messageId: message.id, additions }],
60
+ })
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Read an immutable snapshot; subsequent events do not mutate it.
66
+ * @returns initial declarations and historically resolved additions for the current series.
67
+ */
68
+ snapshot(): ToolHistory {
69
+ // Sessions written before tool-update emission have headers without matching updates.
70
+ if (this.active.length !== this.available.size || this.active.some(tool => !this.available.has(tool.name))) {
71
+ return deepFreeze({ tools: this.active, updates: [] })
72
+ }
73
+ return this.history
74
+ }
75
+ }
package/src/types.ts ADDED
@@ -0,0 +1,574 @@
1
+ import { brandNumber, brandString, type Branded, type BrandedNumber } from '@deepseek-ai/dsh-brand'
2
+ import type {
3
+ AssistantMessage,
4
+ DeveloperMessage,
5
+ AssistantStreamRecord,
6
+ ToolCallId,
7
+ LlmCallConfig,
8
+ LlmCallConfigAdapterDefaults,
9
+ LlmFailure,
10
+ SystemMessage,
11
+ SystemPromptUpdate,
12
+ TokenUsage,
13
+ ToolResultMessage,
14
+ ToolSchema,
15
+ UserMessage,
16
+ } from '@deepseek-ai/dsh-llm'
17
+ import type { TypertResource } from '@deepseek-ai/dsh-typert-protocol'
18
+ import type { JsonValue } from '@deepseek-ai/dsh-util-values'
19
+
20
+ /** Identifies one session in the store (and its persistence artifacts). */
21
+ export type SessionId = Branded<'SessionId'>
22
+
23
+ /**
24
+ * Brand a string as a {@link SessionId}.
25
+ * @param id - the raw session id string.
26
+ * @returns the same string with the session-id brand.
27
+ */
28
+ export function SessionId(id: string): SessionId {
29
+ return brandString<SessionId>(id)
30
+ }
31
+
32
+ /** Sequence number of one existing event in a Session log. */
33
+ export type SessionSeq = BrandedNumber<'SessionSeq'>
34
+
35
+ /**
36
+ * Admit a numeric value as an existing Session event position.
37
+ * @param value - non-negative safe integer admitted by the owning log operation.
38
+ * @returns the same number with the Session-sequence brand.
39
+ */
40
+ export function SessionSeq(value: number): SessionSeq {
41
+ if (!Number.isSafeInteger(value) || value < 0 || Object.is(value, -0)) {
42
+ throw new TypeError(`SessionSeq must be a non-negative safe integer, got ${String(value)}`)
43
+ }
44
+ return brandNumber<SessionSeq>(value)
45
+ }
46
+
47
+ /** A Session log gap, prefix length, or read offset, which may equal the event count. */
48
+ export type SessionLogOffset = BrandedNumber<'SessionLogOffset'>
49
+
50
+ /**
51
+ * Admit a numeric value as a Session log offset.
52
+ * @param value - non-negative safe integer used as a gap or prefix length.
53
+ * @returns the same number with the Session-log-offset brand.
54
+ */
55
+ export function SessionLogOffset(value: number): SessionLogOffset {
56
+ if (!Number.isSafeInteger(value) || value < 0 || Object.is(value, -0)) {
57
+ throw new TypeError(`SessionLogOffset must be a non-negative safe integer, got ${String(value)}`)
58
+ }
59
+ return brandNumber<SessionLogOffset>(value)
60
+ }
61
+
62
+ /** Inclusive Session event watermark, or `-1` before any event exists. */
63
+ export type SessionSeqCursor = SessionSeq | -1
64
+
65
+ /** One existing Session event position, or explicit absence. */
66
+ export type OptionalSessionSeq = SessionSeq | null
67
+
68
+ /**
69
+ * Current logical Session format version, stamped into every newly written
70
+ * {@link SessionHeader}. Current Session and persistence code accept only this
71
+ * value; header-only readers classify supported historical formats, while an
72
+ * event-body read composes the build-static adjacent chain and publishes only
73
+ * this final generation before constructing a Session.
74
+ *
75
+ * The version is a single monotonic integer with no major/minor split. Retain
76
+ * it when older readers resume correctly, preserve safely ignorable records,
77
+ * or refuse required features before execution, mutation, or tail repair.
78
+ * New readers must preserve historical meanings, including logs mixing old
79
+ * and new record representations. Bump when an existing discriminator cannot
80
+ * prevent unsafe interpretation; a structural difference alone is insufficient.
81
+ * Unknown fields and values require evidence of the released reader's actual
82
+ * admission behavior. The per-event {@link SessionEvent.ignorable} marker
83
+ * governs unknown event types, not arbitrary nested payload changes.
84
+ * Same-version readers retain earlier representations: the adjacent migration
85
+ * chain does not run when stored and current versions match. A subsequent
86
+ * migration must account for all supported representations of its source.
87
+ * Compatibility review and the retained migration obligations are recorded in
88
+ * `.agents/notes/implemented/process/2026-10-08-session-reader-compatibility-review.md`.
89
+ */
90
+ export const SESSION_FORMAT_VERSION = 4
91
+
92
+ /**
93
+ * Immutable validated storage metadata, kept outside the conversation event log.
94
+ */
95
+ export interface SessionHeader {
96
+ /**
97
+ * Current logical format version, stamped from {@link SESSION_FORMAT_VERSION}.
98
+ * Historical physical headers are translated before entering this interface.
99
+ */
100
+ readonly version: typeof SESSION_FORMAT_VERSION
101
+ /** The session's id (mirrors the {@link Session}'s id). */
102
+ readonly id: SessionId
103
+ /** Non-negative safe-integer Unix epoch milliseconds when the session was created. */
104
+ readonly createdAt: number
105
+ /** Absolute working directory the session was created in (if any). */
106
+ readonly cwd?: string
107
+ /**
108
+ * Direct creator of a subagent Session. Ordinary forks omit this field;
109
+ * historical ordinary forks may retain their source id here. Presence alone
110
+ * does not classify a Session as a subagent; use `origin` for that distinction.
111
+ */
112
+ readonly parentSession?: SessionId
113
+ /**
114
+ * Whether this Session contains a fork-inherited event prefix. The exact prefix
115
+ * length is Session state rather than ordinary header metadata.
116
+ */
117
+ readonly isSeeded: boolean
118
+ /**
119
+ * Coarse product classification for a session created as a subagent child.
120
+ * This is presentation metadata, not proof that the child is continuable.
121
+ */
122
+ readonly origin?: 'subagent'
123
+ /**
124
+ * Delegation depth: absent (zero) for a top-level session, parent depth + 1
125
+ * for a subagent child. Persisted so a recursion budget survives restart and
126
+ * resume — a runtime-only depth would reset a resumed child to top-level.
127
+ */
128
+ readonly delegationDepth?: number
129
+ /**
130
+ * Id of the agent preset this session's agent was composed from, when the
131
+ * deployment composes per session. Durable because the preset decides the
132
+ * session's tools and prompt: a resume that restored a different composition
133
+ * would replay history the model can no longer act on.
134
+ */
135
+ readonly agentPreset?: string
136
+ }
137
+
138
+ /**
139
+ * Options for creating a {@link Session} via the store. `seed` replays/forks
140
+ * an existing event log; `meta` carries the caller-supplied storage fields the
141
+ * store folds into a {@link SessionHeader}.
142
+ */
143
+ export interface CreateSessionOptions {
144
+ /** Initial replay or fork history supplied at construction. */
145
+ readonly seed?: readonly SessionEvent[]
146
+ /**
147
+ * Exact fork-inherited prefix length when `meta.isSeeded` is true. The
148
+ * constructor appends the child-owned tagged marker at the cut unless
149
+ * the seed already includes it followed by child-owned fork closers.
150
+ */
151
+ readonly inheritedEventCount?: SessionLogOffset
152
+ /**
153
+ * Storage metadata read once before publication. `isSeeded` marks fork
154
+ * lineage; supplying replay history alone does not make it inherited.
155
+ */
156
+ readonly meta?: {
157
+ readonly cwd?: string
158
+ readonly parentSession?: SessionId
159
+ readonly createdAt?: number
160
+ readonly isSeeded?: boolean
161
+ readonly origin?: 'subagent'
162
+ readonly delegationDepth?: number
163
+ readonly agentPreset?: string
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Aliasing state of an adoptable Session seed. `shared-frozen` permits deeply
169
+ * frozen aliases plus independently owned unfrozen values in the same seed.
170
+ */
171
+ export type SessionSeedEventState = 'detached' | 'shared-frozen'
172
+
173
+ /**
174
+ * Adoptable storage values transferred to {@link SessionStore.prepare}
175
+ * without another copy or freeze pass.
176
+ */
177
+ export interface RestoredSessionOptions {
178
+ /** Events that are independently owned or already deeply frozen. */
179
+ readonly seed: SessionEvent[]
180
+ /** Independently owned storage metadata to validate and freeze in place. */
181
+ readonly meta: SessionHeader
182
+ /** Exact number of fork-inherited leading events decoded from storage. */
183
+ readonly inheritedEventCount: SessionLogOffset
184
+ /** Aliasing state carried from the operation that produced the seed. */
185
+ readonly eventState: SessionSeedEventState
186
+ }
187
+
188
+ /** Inputs accepted while constructing an unpublished Session. */
189
+ export type PrepareSessionOptions =
190
+ | (CreateSessionOptions & { readonly eventState?: undefined })
191
+ | RestoredSessionOptions
192
+
193
+ /** Why an active agent driver was cancelled. */
194
+ export type AgentCancelCause =
195
+ | { readonly kind: 'user' }
196
+ | { readonly kind: 'parent' }
197
+ | { readonly kind: 'hook'; readonly reason: string }
198
+ | { readonly kind: 'disposed' }
199
+
200
+ /** Durable cancellation cause, including imports whose original coarse record carried no cause. */
201
+ export type TurnEndCancelCause = AgentCancelCause | { readonly kind: 'legacy' }
202
+
203
+ /**
204
+ * Why a turn ended. Merge-extensible sum type.
205
+ */
206
+ export interface TurnEndReasonMap {
207
+ completed: { kind: 'completed' }
208
+ /** A cancellation request interrupted the live turn. */
209
+ aborted: { kind: 'aborted'; reason: TurnEndCancelCause }
210
+
211
+ blocked: { kind: 'blocked' }
212
+ /**
213
+ * The turn failed. `error` is always a structured failure: the `LlmError`
214
+ * facts verbatim, or `{ message: errorChain(error), code }` flattened from
215
+ * any other error, where `code` is `TOOL_ACTIVATION_FAILED` for a failed
216
+ * tool condition and `UNKNOWN` otherwise.
217
+ */
218
+ error: { kind: 'error'; error: LlmFailure }
219
+ /** At least one step reached its output-token ceiling, even if a plugin continued the turn. */
220
+ 'max-tokens': { kind: 'max-tokens' }
221
+ /**
222
+ * A crash-orphaned turn was closed after the fact: agent-loop resume appends
223
+ * this closer for a stored log whose last turn never ended, and session-query
224
+ * synthesizes it on cold reads. The loop never emits this marker live, and
225
+ * the events recorded before the crash remain intact.
226
+ */
227
+ interrupted: { kind: 'interrupted' }
228
+ /**
229
+ * Fork-seed construction closed a turn that was still open at the fork
230
+ * boundary in the source session. Only fork seeds carry this marker — the
231
+ * loop never emits it — and the source events before the boundary remain
232
+ * intact in the child.
233
+ */
234
+ forked: { kind: 'forked' }
235
+ }
236
+
237
+ /** The union over {@link TurnEndReasonMap} — why a turn ended; plugins extend it by merging variants into the map. */
238
+ export type TurnEndReason = TurnEndReasonMap[keyof TurnEndReasonMap]
239
+
240
+ /**
241
+ * Logged request state outside derived history: call config and tools. The
242
+ * system prompt is derived history — surface node 0, a `system/message` event.
243
+ * The latest full `request/header` snapshot reconstructs the header; canonical
244
+ * empty optional fields are absent.
245
+ */
246
+ export interface EpochHeader {
247
+ /** The conversation's call configuration (provider, model, reasoning effort, and sampling scalars). */
248
+ config: LlmCallConfig
249
+ /** Effective config fields materialized from the exact adapter rather than proposed by a caller. */
250
+ adapterDefaults?: LlmCallConfigAdapterDefaults
251
+ /** Assembled tool schemas; absent for a tool-less request. */
252
+ tools?: ToolSchema[]
253
+ /** Retired request text; system prompts belong to system/message events.
254
+ * @persistenceReserved
255
+ */
256
+ system?: never
257
+ }
258
+
259
+ /** Registration-bound metadata for one resolved model route. */
260
+ export interface RequestContext {
261
+ /** Registered provider route the metadata belongs to. */
262
+ provider: string
263
+ /** Provider-owned model id the metadata belongs to. */
264
+ model: string
265
+ /** Maximum combined request and response context in tokens, when advertised. */
266
+ contextWindow?: number
267
+ /** `'in-history'` when the route reads the latest `system` message at any position as the effective system prompt. */
268
+ systemPromptUpdate?: SystemPromptUpdate
269
+ }
270
+
271
+ /**
272
+ * Why a `request/header` snapshot was appended: `'initial'` — the log's first
273
+ * header (a new conversation); `'resume'` — a loop instance's first request
274
+ * over a log that already has header events (process restart, fork seed);
275
+ * `'change'` — a later request used a different header; `'series'` — an unchanged
276
+ * header began an explicitly distinct message series or followed a surface
277
+ * replacement. Other reasons carry `startsSeries` when a new series coincides.
278
+ */
279
+ export type RequestHeaderReason = 'initial' | 'resume' | 'change' | 'series'
280
+
281
+ /**
282
+ * The merge-extensible, append-only source of truth for an agent interaction.
283
+ * Message history is derived from this log. Every event is lossless JSON and
284
+ * sequence numbers stay contiguous. Assistant attempt events embed their exact
285
+ * compact raw streams so persistence stores one durable settlement per attempt.
286
+ */
287
+ export interface SessionEventMap {
288
+ /**
289
+ * Opens turn `turn` before the loop claims queued input or runs pre-step.
290
+ * Rejection, empty input, cancellation, or failure may close it with no
291
+ * step; otherwise the following identified `user/message` event or batch
292
+ * records the messages entering the step.
293
+ */
294
+ 'turn/start': { turn: number }
295
+ /**
296
+ * Closes turn `turn` with the {@link TurnEndReason} that ended it. A turn
297
+ * with no entered step has no `step/start` or `step/end`. The loop does not await a
298
+ * flush at turn boundaries: `dsh-session-checkpoint-policy` owns the
299
+ * per-request durability checkpoint, and consumers that read storage after
300
+ * `whenIdle()` flush themselves. Success commits the turn; rejection is
301
+ * reported live and does not prevent later work.
302
+ */
303
+ 'turn/end': { turn: number; reason: TurnEndReason }
304
+ /** Opens step `step` of turn `turn` — one model call plus the tool executions it requested. */
305
+ 'step/start': { turn: number; step: number }
306
+ /** Closes step `step` of turn `turn`. */
307
+ 'step/end': { turn: number; step: number }
308
+ /**
309
+ * A user-role message on the model-visible surface: a direct human prompt
310
+ * (the queued message claimed for this turn), a synthetic `agent.inject()`
311
+ * context (file-change notices, subdir AGENTS.md, skill content, cron
312
+ * notifications, …), or an entered goal continuation round. All three
313
+ * project their `content` verbatim; `source` tells them apart.
314
+ */
315
+ 'user/message': UserMessage
316
+ /** An incremental agent session change admitted at the named turn and step. */
317
+ 'developer/message': {
318
+ turn: number
319
+ step: number
320
+ message: DeveloperMessage
321
+ /** Earlier request/header defining every tool addition; required exactly when additions are present. */
322
+ headerSeq?: SessionSeq
323
+ }
324
+ /**
325
+ * The rendered system prompt on the model-visible surface. The loop appends
326
+ * the first one as surface node 0 before the step's first `user/message`.
327
+ * A prepared in-history route can append nonempty changes in a continuing
328
+ * series. An incapable route, or a new series whose effective text differs,
329
+ * normalizes text to the first system node; a series start whose latest
330
+ * surviving node already holds the rendering leaves it in place.
331
+ * Normalization empties nonempty later nodes, then rewrites the head if
332
+ * needed, through logged per-node replacements. An empty rendering always
333
+ * clears all active system nodes, leaving no older instructions model-visible.
334
+ * Empty later nodes are dormant and project to no message; an empty head with
335
+ * no active later node records "no system prompt". Restored nonempty text follows
336
+ * the same route and series rule; empty nodes never restore older text.
337
+ */
338
+ 'system/message': { turn: number; step: number; message: SystemMessage }
339
+ /**
340
+ * Assembled assistant message for one step (derived history uses this).
341
+ * Carries the step's `usage` when the adapter reported token accounting, so
342
+ * the model output and its accounting travel together (there is no separate
343
+ * usage record). `usage` is absent when the adapter reported none. A turn
344
+ * cancelled mid-stream finalizes its delivered text/reasoning prefix as this
345
+ * event with `interrupted: true`; undispatched tool calls are absent. The
346
+ * marker distinguishes that prefix without re-deriving interruption from turn
347
+ * boundaries. An aborted turn with no such event streamed no visible content.
348
+ */
349
+ 'assistant/message': {
350
+ turn: number
351
+ step: number
352
+ message: AssistantMessage
353
+ /** Exact timed model stream, compacted without joining delta boundaries. */
354
+ stream: AssistantStreamRecord[]
355
+ usage?: TokenUsage
356
+ interrupted?: true
357
+ }
358
+ /**
359
+ * One model attempt that committed no surface message. The embedded stream
360
+ * preserves a failed, retried, cancelled, or stream-error attempt that
361
+ * reached settlement without fabricating model-visible history.
362
+ */
363
+ 'assistant/attempt': { turn: number; step: number; stream: AssistantStreamRecord[] }
364
+ /**
365
+ * The model requested one tool invocation: `name` with the raw `arguments`
366
+ * JSON string exactly as the model produced it (unparsed). `callId` pairs the
367
+ * call with its `tool/result`.
368
+ */
369
+ 'tool/call': { turn: number; step: number; callId: ToolCallId; name: string; arguments: string }
370
+ /**
371
+ * A completed tool call's model-facing result, optional internal failure
372
+ * identity and user-facing reason, and optional tool-private `meta`
373
+ * presentation payload. The reason remains outside the model-facing message.
374
+ * `meta` is
375
+ * opaque to the core (the producing tool owns its shape and reads it back in
376
+ * `presentResult`) but MUST be JSON-serializable: `Session.append`
377
+ * runtime-validates all event data with `isJsonValue`, so a non-serializable
378
+ * `meta` is rejected at the source, and the durable log reproduces the
379
+ * identical card on replay. Absent
380
+ * unless the tool attaches one (e.g. `dsh-tool-fs` carries its result-time
381
+ * contextual diff here).
382
+ */
383
+ 'tool/result': {
384
+ turn: number
385
+ step: number
386
+ message: ToolResultMessage
387
+ /**
388
+ * Optional failure identity and raw user-facing reason, outside model content;
389
+ * allowed only when the message has `isError: true`.
390
+ */
391
+ error?: { name: string; code: string; reason?: string }
392
+ meta?: JsonValue
393
+ }
394
+ /**
395
+ * Full header for the next request, appended inside its step before dispatch.
396
+ * It is log-only; the latest snapshot reconstructs the request header.
397
+ */
398
+ 'request/header': {
399
+ header: EpochHeader
400
+ reason: RequestHeaderReason
401
+ /** This request begins a distinct model-message series, independently of the header reason. */
402
+ startsSeries?: true
403
+ }
404
+ /**
405
+ * Route metadata for the next request, logged only when the route, capacity,
406
+ * or system prompt update mode changes. It does not participate in request
407
+ * reconstruction or header equality. Prompt admission uses the bound prepared
408
+ * call's capability, not this snapshot from an earlier request.
409
+ */
410
+ 'request/context': RequestContext
411
+ /**
412
+ * Separates inherited or restored history from later lifecycle-owned work.
413
+ * This log-only marker need not be at {@link Session.firstLiveSeq}: a fork
414
+ * seed can already contain its tagged marker and child-owned synthetic
415
+ * closers before construction.
416
+ *
417
+ * A fresh fork child owns one `{ inherited: true }` marker at its exact
418
+ * inherited-prefix cut, even when that prefix ends in an ancestor marker.
419
+ * `buildForkSeed` appends that marker before any synthetic closers; the
420
+ * `Session` constructor supplies it when given only the inherited prefix.
421
+ * The last tagged marker is the current Session's cut; untagged markers
422
+ * keep ordinary restore and replay lifecycle boundaries.
423
+ *
424
+ * Only the `Session` constructor and `buildForkSeed` may create this marker.
425
+ * Session append does not reject other writers, so a plugin appending one
426
+ * would silently classify every live bracket before it as seed history.
427
+ *
428
+ * An owner of a standalone open/close bracket (`compaction/start` …
429
+ * `compaction/end`) reads it because seed history and live work are otherwise
430
+ * byte-identical: an unmatched opening marker before this event belongs to
431
+ * an ended lifecycle, whatever ended it. NOT a liveness signal about other
432
+ * writers — a concurrently live session holds its own boundary elsewhere,
433
+ * so tolerating concurrent writers needs a signal beyond the log.
434
+ */
435
+ 'session/end-seed': { inherited?: true }
436
+ }
437
+
438
+ /** The appendable event-type keys of {@link SessionEventMap}, plugin-merged extensions included. */
439
+ export type SessionEventType = keyof SessionEventMap
440
+
441
+ /**
442
+ * The subset of {@link SessionEventType} values whose events produce LLM
443
+ * messages and are eligible to appear on the ordered surface. Only these
444
+ * event types may carry {@link SurfaceOp}; system, user, and tool events may also cite
445
+ * earlier sources through {@link SessionEvent.sourceEventSeqs}.
446
+ */
447
+ export type SurfaceEventType =
448
+ | 'system/message'
449
+ | 'developer/message'
450
+ | 'user/message'
451
+ | 'assistant/message'
452
+ | 'tool/result'
453
+
454
+ /** A message-producing event carrying its required surface operation. */
455
+ export type SurfaceEvent = SessionEvent<SurfaceEventType>
456
+
457
+ /**
458
+ * How a session event entered the ordered surface. Only valid on
459
+ * {@link SurfaceEventType} events.
460
+ *
461
+ * - `'append'`: added to the tail — normal path for user/assistant/tool
462
+ * messages.
463
+ * - `{ op: 'replace', startSeq, endSeq }`: replaces surface nodes from `startSeq`
464
+ * (inclusive) through `endSeq` (inclusive) with this node. Both must exist as
465
+ * surface nodes in the current surface. `startSeq === endSeq` replaces a single
466
+ * node. The node's {@link SessionEvent.sourceEventSeqs} must include every
467
+ * shadowed surface node. Used by compaction; any surface-replacing producer
468
+ * may use it.
469
+ */
470
+ export type SurfaceOp =
471
+ | 'append'
472
+ | { op: 'replace'; startSeq: SessionSeq; endSeq: SessionSeq }
473
+
474
+ /**
475
+ * Surface placement and cited source-event seqs for {@link Session.append}. Required on
476
+ * message-producing events and forbidden on log-only events.
477
+ */
478
+ export type SurfaceIntent<T extends SurfaceEventType = SurfaceEventType> = {
479
+ surfaceOp: SurfaceOp
480
+ } & (T extends 'assistant/message' ? {
481
+ /** Assistant messages embed their provider stream instead of citing source events. */
482
+ sourceEventSeqs?: never
483
+ } : {
484
+ /** Complete non-empty set of known earlier source-event seqs. */
485
+ sourceEventSeqs?: SessionSeq[]
486
+ })
487
+
488
+ /** One positional event call proposed for append, without assigned sequence or time. */
489
+ export type SessionEventInput = {
490
+ [K in SessionEventType]: readonly [type: K, data: SessionEventMap[K], ...opts: K extends SurfaceEventType ? [opts: SurfaceIntent<K>] : []]
491
+ }[SessionEventType]
492
+
493
+ /**
494
+ * One immutable entry in the session log.
495
+ *
496
+ * A proper discriminated union over `type` (not independent `type`/`data`
497
+ * unions), so `switch (event.type)` narrows `event.data` without casts.
498
+ *
499
+ * The {@link sourceEventSeqs} and {@link surfaceOp} fields are conditional:
500
+ * they only exist on {@link SurfaceEventType} variants (`system/message`, `user/message`,
501
+ * `assistant/message`, `tool/result`).
502
+ * Non-surface events (boundary markers, attempts, errors) never carry
503
+ * surface metadata — the compiler enforces this at `Session.append()`
504
+ * call sites.
505
+ */
506
+ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
507
+ [K in SessionEventType]: {
508
+ type: K
509
+ /** Monotonic sequence number within the session. */
510
+ seq: SessionSeq
511
+ /** Unix epoch milliseconds. */
512
+ time: number
513
+ data: SessionEventMap[K]
514
+ /**
515
+ * Marks an event a reader may safely skip when it does not recognize
516
+ * `type`. Absent means required: a reader meeting an unrecognized type
517
+ * without this marker MUST refuse to reconstruct the session instead of
518
+ * silently dropping the event, because an unrecognized required event may
519
+ * change how the rest of the log is interpreted. A writer sets `true` only
520
+ * on purely informational records whose loss cannot affect reconstruction;
521
+ * defaulting to required means a forgotten marker over-refuses (an
522
+ * inconvenience) rather than silently resuming a gutted session.
523
+ */
524
+ ignorable?: true
525
+ } & (K extends SurfaceEventType ? SurfaceIntent<K> : {
526
+ surfaceOp?: never
527
+ sourceEventSeqs?: never
528
+ })
529
+ }[T]
530
+
531
+ /**
532
+ * Event type of an experimental plugin record: `plugin:` followed by
533
+ * slash-separated segments of lowercase letters, digits, `.`, `_`, and `-`,
534
+ * each starting with a letter or digit. A record type is never a
535
+ * {@link SessionEventMap} member.
536
+ */
537
+ export type PluginRecordType = `plugin:${string}`
538
+
539
+ /**
540
+ * Payloads written by experimental packages, keyed by their `plugin:` record
541
+ * names. Packages augment this map to type writes and enter the current plugin
542
+ * record catalog; these declarations do not enter {@link SessionEventMap} or
543
+ * released persistence schemas. Stored records still require owner validation.
544
+ */
545
+ export interface PluginRecordMap {}
546
+
547
+ /**
548
+ * One committed experimental plugin record, read from the log by
549
+ * `pluginRecordOf`. The record's owner validates `data` before use, because a
550
+ * restored record carries whatever JSON an earlier build of its owner wrote,
551
+ * or a V3 event that a format migration renamed into the `plugin:` namespace held.
552
+ */
553
+ export interface PluginRecord {
554
+ /** The record type, chosen by the owning plugin. */
555
+ readonly type: PluginRecordType
556
+ /** The record's position in the Session log. */
557
+ readonly seq: SessionSeq
558
+ /** Unix epoch milliseconds at append. */
559
+ readonly time: number
560
+ /** The JSON payload as committed. */
561
+ readonly data: unknown
562
+ }
563
+
564
+ declare module '@deepseek-ai/dsh-typert-protocol' {
565
+ interface TypertResourceMap {
566
+ /** One Session; the Session Controller registers its default `sessionId` wire field. */
567
+ session: TypertResource<SessionId>
568
+ }
569
+
570
+ interface RemoteErrorDetailsMap {
571
+ /** The named Session does not exist; produced by every layer that resolves a SessionId. */
572
+ 'session/not-found': { readonly sessionId: SessionId }
573
+ }
574
+ }