@deepseek-ai/dsh-session 0.1.2-rc.1 → 0.1.5-alpha.1

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.
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Service } from "@deepseek-ai/cordis";
2
2
  import { isAbsolute } from "node:path";
3
3
  import { brandNumber, brandString } from "@deepseek-ai/dsh-brand";
4
- import { deepFreeze, snapshotJsonValue } from "@deepseek-ai/dsh-util-values";
4
+ import { assertNever, deepFreeze, snapshotJsonValue } from "@deepseek-ai/dsh-util-values";
5
5
  import { scopeOf, scopeTarget } from "@deepseek-ai/dsh-scope";
6
6
  import { callConfigEquals } from "@deepseek-ai/dsh-llm";
7
7
  //#region lib/types/types.js
@@ -32,11 +32,11 @@ function SessionLogOffset(value) {
32
32
  return brandNumber(value);
33
33
  }
34
34
  /**
35
- * The on-disk session format version, stamped into every newly-written {@link SessionHeader}
36
- * and enforced by every persistence backend on load. The single source of truth for the
37
- * version — write sites and the load-time check all read it.
38
- * While the harness is unreleased it is pinned at `0`: no compatibility is
39
- * implied, incompatible logs are rejected, and no migration is provided.
35
+ * Current logical Session format version, stamped into every newly written
36
+ * {@link SessionHeader}. Current Session and persistence code accept only this
37
+ * value; header-only readers classify supported historical formats, while an
38
+ * event-body read composes the build-static adjacent chain and publishes only
39
+ * this final generation before constructing a Session.
40
40
  *
41
41
  * The version is a single monotonic integer with no major/minor split. Whether
42
42
  * a bump is needed is decided by what the WRITER emits, never by what a newer
@@ -49,12 +49,89 @@ function SessionLogOffset(value) {
49
49
  * Adding an ordinary event type does not bump — the per-event
50
50
  * {@link SessionEvent.ignorable} guard covers vocabulary growth instead. When
51
51
  * in doubt, bump: a near-identity upgrade step is almost free, a missed bump
52
- * makes older runtimes read new logs wrong silently. The full mechanism
53
- * (upgrade-step chain, in-memory view conversion, migrate-on-continue) is
54
- * recorded in the session-log-version-mechanism Agent Note
55
- * (`.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md`).
52
+ * makes older runtimes read new logs wrong silently. The released migration,
53
+ * immutable prior-generation, and current fast-path rules are recorded in
54
+ * `.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md`.
56
55
  */
57
- const SESSION_FORMAT_VERSION = 0;
56
+ const SESSION_FORMAT_VERSION = 3;
57
+ //#endregion
58
+ //#region lib/types/known-event-types.js
59
+ /**
60
+ * GENERATED by `scripts/gen-persistence-catalog.ts` — do not edit by hand; run
61
+ * `pnpm run gen-persistence-catalog` to regenerate (verified fresh by
62
+ * `pnpm run verify-persistence-catalog`, part of `doc-sync`).
63
+ * @module @deepseek-ai/dsh-session/known-event-types
64
+ */
65
+ /**
66
+ * Every `SessionEventMap` member declared in this repository — the event
67
+ * vocabulary this build understands. The persistence read path refuses to
68
+ * interpret a log containing a type outside this set unless the event
69
+ * carries the envelope's `ignorable` marker (see `SessionEvent.ignorable`
70
+ * in `./types.ts`): such a log was likely written by a newer harness, and
71
+ * silently skipping a required event would reconstruct a wrong session.
72
+ * Downstream (out-of-repo) plugin events are outside this list by
73
+ * construction. The persisted `SessionEvent.ignorable` marker is the
74
+ * compatibility mechanism; event-name registration was rejected because
75
+ * it does not classify omission safety and would make reads
76
+ * composition-dependent. The rationale is in
77
+ * `.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.md`.
78
+ */
79
+ const KNOWN_SESSION_EVENT_TYPES = new Set([
80
+ "agent-preset/selected",
81
+ "agent/inbox/spliced",
82
+ "approval/asked",
83
+ "approval/decided",
84
+ "approval/policy",
85
+ "assistant/attempt",
86
+ "assistant/message",
87
+ "command/done",
88
+ "command/run",
89
+ "compaction/end",
90
+ "compaction/prune",
91
+ "compaction/start",
92
+ "compaction/summary",
93
+ "feedback/message-delete",
94
+ "feedback/message-put",
95
+ "feedback/record",
96
+ "goal/change",
97
+ "hook/invoked",
98
+ "hook/result",
99
+ "llm/retry",
100
+ "llm/retry-started",
101
+ "model/selection",
102
+ "permission/preset",
103
+ "plan/mode",
104
+ "request/context",
105
+ "request/header",
106
+ "sandbox/mode",
107
+ "schedule/change",
108
+ "session-log-deepseek/delivery-accepted",
109
+ "session/end-seed",
110
+ "session/title",
111
+ "session/title-llm-request",
112
+ "step/end",
113
+ "step/start",
114
+ "subagent/descriptor",
115
+ "subagent/model-selection-policy",
116
+ "system/message",
117
+ "team/member",
118
+ "team/message/delivered",
119
+ "team/message/queued",
120
+ "team/task",
121
+ "todo/write",
122
+ "tool-workflow/agent-end",
123
+ "tool-workflow/agent-start",
124
+ "tool-workflow/run-end",
125
+ "tool-workflow/run-start",
126
+ "tool/call",
127
+ "tool/ptc-dispatch",
128
+ "tool/ptc-dispatch-start",
129
+ "tool/result",
130
+ "turn/end",
131
+ "turn/start",
132
+ "user/message",
133
+ "web/deepseek-search-llm-request"
134
+ ]);
58
135
  //#endregion
59
136
  //#region lib/types/surface.js
60
137
  /**
@@ -68,6 +145,7 @@ const SESSION_FORMAT_VERSION = 0;
68
145
  */
69
146
  /** Runtime counterpart of the message-producing event union. */
70
147
  const SURFACE_EVENT_TYPES = new Set([
148
+ "system/message",
71
149
  "user/message",
72
150
  "assistant/message",
73
151
  "tool/result"
@@ -75,7 +153,7 @@ const SURFACE_EVENT_TYPES = new Set([
75
153
  /**
76
154
  * Whether an event type can join the model-visible surface.
77
155
  * @param type - event type to test.
78
- * @returns true for one of the three message-producing event types.
156
+ * @returns true for one of the four message-producing event types.
79
157
  */
80
158
  function isSurfaceEligibleType(type) {
81
159
  return SURFACE_EVENT_TYPES.has(type);
@@ -115,7 +193,7 @@ function isReplacementSurfaceEvent(event) {
115
193
  }
116
194
  /**
117
195
  * Project a single event into the LLM message it derives to, or null when it
118
- * produces none — a non-surface event (chunk, boundary, log-only record) or an
196
+ * produces none — a non-surface event (attempt, boundary, log-only record) or an
119
197
  * empty-content assistant/message (which exists only to host usage). This is
120
198
  * THE per-node projection rule: `Session.deriveMessages` folds it over the
121
199
  * live surface, external reconstructors and pure projections fold the same
@@ -129,6 +207,7 @@ function isReplacementSurfaceEvent(event) {
129
207
  function deriveEventMessage(event) {
130
208
  switch (event.type) {
131
209
  case "user/message": return event.data;
210
+ case "system/message":
132
211
  case "assistant/message":
133
212
  if (event.data.message.content.length === 0) return null;
134
213
  return event.data.message;
@@ -136,6 +215,36 @@ function deriveEventMessage(event) {
136
215
  default: return null;
137
216
  }
138
217
  }
218
+ /** Whether a payload field is a JSON object rather than an array or scalar. */
219
+ function isRecord(value) {
220
+ return typeof value === "object" && value !== null && !Array.isArray(value);
221
+ }
222
+ /**
223
+ * Reject noncanonical request-header fields and contradictory tool failure metadata.
224
+ * This does not validate complete event payloads or embedded provider streams.
225
+ * @param event - event whose locally related payload fields are inspected.
226
+ * @param subject - event location to include in validation errors.
227
+ * @throws when request data/header is not an object, optional header fields are empty, or tool failure metadata contradicts its message.
228
+ */
229
+ function validateSessionEventData(event, subject) {
230
+ const data = event.data;
231
+ if (event.type === "request/header") {
232
+ if (!isRecord(data)) throw new Error(`${subject} data must be an object`);
233
+ const header = data["header"];
234
+ if (!isRecord(header)) throw new Error(`${subject} header must be an object`);
235
+ if (Object.hasOwn(header, "system")) throw new Error(`${subject} must omit header.system; use system/message`);
236
+ if (Array.isArray(header["tools"]) && header["tools"].length === 0) throw new Error(`${subject} must omit empty tools`);
237
+ const defaults = header["adapterDefaults"];
238
+ if (isRecord(defaults) && Object.keys(defaults).length === 0) throw new Error(`${subject} must omit empty adapterDefaults`);
239
+ } else if (event.type === "tool/result") {
240
+ if (!isRecord(data)) throw new Error(`${subject} data must be an object`);
241
+ if (data["error"] === void 0) return;
242
+ const message = data["message"];
243
+ const content = isRecord(message) ? message["content"] : void 0;
244
+ const block = Array.isArray(content) ? content[0] : void 0;
245
+ if (!isRecord(block) || block["isError"] !== true) throw new Error(`${subject} error requires message content[0].isError === true`);
246
+ }
247
+ }
139
248
  /** Create an empty surface fold state. */
140
249
  function createFoldState() {
141
250
  return {
@@ -150,12 +259,13 @@ function isEventSeq(value) {
150
259
  /** Whether a runtime value is the exact positional-replacement shape. */
151
260
  function isReplaceOp(value) {
152
261
  const op = value;
153
- return Object.keys(op).length === 3 && Object.hasOwn(op, "op") && Object.hasOwn(op, "start") && Object.hasOwn(op, "end") && op["op"] === "replace" && isEventSeq(op["start"]) && isEventSeq(op["end"]);
262
+ return Object.keys(op).length === 3 && Object.hasOwn(op, "op") && Object.hasOwn(op, "startSeq") && Object.hasOwn(op, "endSeq") && op["op"] === "replace" && isEventSeq(op["startSeq"]) && isEventSeq(op["endSeq"]);
154
263
  }
155
264
  /** Validate event-local surface eligibility and return its operation. */
156
265
  function surfaceOpOf(event) {
157
266
  const raw = event;
158
267
  if (!isSurfaceEligibleType(event.type)) {
268
+ if (!KNOWN_SESSION_EVENT_TYPES.has(event.type) && event.ignorable === true) return;
159
269
  if (raw.surfaceOp !== void 0) throw new Error(`session event "${event.type}" is not surface-eligible and cannot carry surfaceOp`);
160
270
  if (raw.sourceEventSeqs !== void 0) throw new Error(`session event "${event.type}" is not surface-eligible and cannot carry sourceEventSeqs`);
161
271
  return;
@@ -170,10 +280,11 @@ function surfaceOpOf(event) {
170
280
  /** Validate cited source-event seqs against prior log entries and the replacement range. */
171
281
  function assertProvenance(event, shadowedSeqs) {
172
282
  const raw = event.sourceEventSeqs;
283
+ if (event.type === "assistant/message" && raw !== void 0) throw new Error("assistant/message embeds its source stream and cannot carry sourceEventSeqs");
173
284
  const sources = /* @__PURE__ */ new Set();
174
285
  if (raw !== void 0) {
175
286
  if (!Array.isArray(raw)) throw new Error(`sourceEventSeqs on event at seq ${event.seq} must be an array when present`);
176
- if (raw.length === 0 && event.type !== "assistant/message") throw new Error("sourceEventSeqs must not be empty except on assistant/message");
287
+ if (raw.length === 0) throw new Error("sourceEventSeqs must not be empty");
177
288
  let nonEarlierSource;
178
289
  for (const source of raw) {
179
290
  if (!isEventSeq(source)) throw new Error(`session event "${event.type}" sourceEventSeqs must densely contain non-negative safe integers`);
@@ -186,13 +297,26 @@ function assertProvenance(event, shadowedSeqs) {
186
297
  const missing = shadowedSeqs.filter((seq) => !sources.has(seq));
187
298
  if (missing.length > 0) throw new Error(`surface replace: sourceEventSeqs must include every shadowed surface node; missing ${missing.join(", ")}`);
188
299
  }
300
+ /**
301
+ * Validate one event's surface metadata without checking membership in a log or surface.
302
+ * @param event - event whose marker and source sequence values are inspected.
303
+ * Unknown ignorable records retain opaque metadata and never change the surface.
304
+ * @returns the validated operation, or undefined for a log-only or unknown ignorable event.
305
+ * @throws when metadata violates event-local eligibility, marker, or source-sequence rules.
306
+ */
307
+ function validateSurfaceMetadata(event) {
308
+ const op = surfaceOpOf(event);
309
+ if (op !== void 0 && op !== "append" && (op.startSeq >= event.seq || op.endSeq >= event.seq)) throw new Error(`surface replace at seq ${event.seq}: startSeq and endSeq must reference earlier events`);
310
+ if (op !== void 0) assertProvenance(event, []);
311
+ return op;
312
+ }
189
313
  /** Locate one replacement range without mutating the current fold state. */
190
314
  function replacementRange(state, op) {
191
- const startIdx = state.nodes.indexOf(op.start);
192
- if (startIdx === -1) throw new Error(`surface replace: start seq ${op.start} not found in surface`);
193
- const endIdx = state.nodes.indexOf(op.end);
194
- if (endIdx === -1) throw new Error(`surface replace: end seq ${op.end} not found in surface`);
195
- if (startIdx > endIdx) throw new Error(`surface replace: start seq ${op.start} (index ${startIdx}) is after end seq ${op.end} (index ${endIdx})`);
315
+ const startIdx = state.nodes.indexOf(op.startSeq);
316
+ if (startIdx === -1) throw new Error(`surface replace: start seq ${op.startSeq} not found in surface`);
317
+ const endIdx = state.nodes.indexOf(op.endSeq);
318
+ if (endIdx === -1) throw new Error(`surface replace: end seq ${op.endSeq} not found in surface`);
319
+ if (startIdx > endIdx) throw new Error(`surface replace: start seq ${op.startSeq} (index ${startIdx}) is after end seq ${op.endSeq} (index ${endIdx})`);
196
320
  return {
197
321
  startIdx,
198
322
  endIdx,
@@ -244,26 +368,35 @@ function assertToolResultRewrite(event, shadowedSeqs, events, baseSeq) {
244
368
  if (!isDeepEqualJson(originalRest, replacementRest)) throw new Error("tool/result surface replacement may change only content");
245
369
  }
246
370
  }
371
+ /**
372
+ * Protect the system prompt at surface node 0. A replacement covering node 0
373
+ * while that node is a `system/message` must itself be a `system/message` over
374
+ * exactly that node; later system nodes carry no protection and a compaction
375
+ * range may shadow them.
376
+ */
377
+ function assertSystemHeadRewrite(event, state, startIdx, shadowedSeqs, events, baseSeq) {
378
+ if (startIdx !== 0) return;
379
+ if (events[state.nodes[0] - baseSeq]?.type !== "system/message") return;
380
+ if (event.type !== "system/message" || shadowedSeqs.length !== 1) throw new Error("surface replace: node 0 holds the system prompt and may be rewritten only by a system/message over exactly that node");
381
+ }
247
382
  /** Validate one event at its replay boundary and prepare its atomic fold transition. */
248
383
  function planSurfaceEvent(state, event, expectedSeq, events, baseSeq) {
249
384
  if (event.seq !== expectedSeq) throw new Error(`session event seq ${event.seq} is not contiguous; expected ${expectedSeq}`);
250
- const surfaceOp = surfaceOpOf(event);
385
+ const surfaceOp = validateSurfaceMetadata(event);
251
386
  if (surfaceOp === void 0) return;
252
- if (surfaceOp === "append") {
253
- assertProvenance(event, []);
254
- return {
255
- kind: "append",
256
- seq: event.seq
257
- };
258
- }
387
+ if (surfaceOp === "append") return {
388
+ kind: "append",
389
+ seq: event.seq
390
+ };
259
391
  const range = replacementRange(state, surfaceOp);
260
392
  assertProvenance(event, range.shadowedSeqs);
261
393
  assertToolResultRewrite(event, range.shadowedSeqs, events, baseSeq);
394
+ assertSystemHeadRewrite(event, state, range.startIdx, range.shadowedSeqs, events, baseSeq);
262
395
  return {
263
396
  kind: "replace",
264
397
  seq: event.seq,
265
- start: surfaceOp.start,
266
- end: surfaceOp.end,
398
+ start: surfaceOp.startSeq,
399
+ end: surfaceOp.endSeq,
267
400
  ...range
268
401
  };
269
402
  }
@@ -371,9 +504,9 @@ var SurfaceManager = class {
371
504
  * @module dsh-session/request-header
372
505
  */
373
506
  /**
374
- * Normalize a header to canonical form: an empty system prompt and empty tool
375
- * list become absent fields, matching how requests are built. Logging, folding,
376
- * and comparison use this one representation.
507
+ * Normalize a header to canonical form: an empty tool list becomes an absent
508
+ * field, matching how requests are built. Logging, folding, and comparison use
509
+ * this one representation.
377
510
  * @param header - the header to normalize (not mutated).
378
511
  * @returns the canonical header.
379
512
  */
@@ -382,7 +515,6 @@ function canonicalHeader(header) {
382
515
  return {
383
516
  config: header.config,
384
517
  ...adapterDefaults?.reasoningEffort === true || adapterDefaults?.maxTokens === true ? { adapterDefaults } : {},
385
- ...header.system !== void 0 && header.system.length > 0 ? { system: header.system } : {},
386
518
  ...header.tools !== void 0 && header.tools.length > 0 ? { tools: header.tools } : {}
387
519
  };
388
520
  }
@@ -394,10 +526,10 @@ function sameSchema(a, b) {
394
526
  * Field-wise equality over canonical headers. Tool schemas compare in order.
395
527
  * @param a - one canonical header.
396
528
  * @param b - the other.
397
- * @returns whether config, system, and tools all match.
529
+ * @returns whether config, adapter defaults, and tools all match.
398
530
  */
399
531
  function headerEquals(a, b) {
400
- if (!callConfigEquals(a.config, b.config) || a.adapterDefaults?.reasoningEffort !== b.adapterDefaults?.reasoningEffort || a.adapterDefaults?.maxTokens !== b.adapterDefaults?.maxTokens || a.system !== b.system) return false;
532
+ if (!callConfigEquals(a.config, b.config) || a.adapterDefaults?.reasoningEffort !== b.adapterDefaults?.reasoningEffort || a.adapterDefaults?.maxTokens !== b.adapterDefaults?.maxTokens) return false;
401
533
  const at = a.tools ?? [];
402
534
  const bt = b.tools ?? [];
403
535
  return at.length === bt.length && at.every((tool, i) => sameSchema(tool, bt[i]));
@@ -575,396 +707,6 @@ function interruptedTurnClosers(events) {
575
707
  return closers;
576
708
  }
577
709
  //#endregion
578
- //#region lib/types/chunk-rows.js
579
- /**
580
- * Lossless row packing for `assistant/chunk` delta runs. Providers stream
581
- * token-sized deltas, so a log stores hundreds of near-identical event lines
582
- * whose JSON envelopes dwarf their payloads (~56× measured on a real DeepSeek
583
- * session). This module packs each run of consecutive same-block delta chunks
584
- * into ONE storage row — `text-chunks`, `reasoning-chunks`, or
585
- * `tool-call-chunks` — and expands rows back to the exact original events.
586
- *
587
- * Packed rows are an encoding vocabulary, NOT session events: they never enter
588
- * `Session.snapshotEvents()`, have no `SessionEventMap` entry, and use bare (slash-less)
589
- * type tags so a reader cannot confuse them with the event taxonomy
590
- * (precedent: the JSONL header line's `session` tag). Persistence and bounded
591
- * history transport both use the codec. The encoder whitelists exact shapes —
592
- * anything it does not fully recognize stays verbatim, so unknown fields or
593
- * future chunk variants lose compression, never data. The decoder validates
594
- * before expanding and fails loud on a malformed row-tagged value instead of
595
- * silently dropping a whole run.
596
- *
597
- * @module @deepseek-ai/dsh-session/chunk-rows
598
- */
599
- /**
600
- * Minimum members before a run packs. Below it a row's envelope rivals the
601
- * event lines it replaces. A format constant, not a tunable: both layouts
602
- * decode identically, so changing it never invalidates stored logs.
603
- */
604
- const MIN_RUN = 3;
605
- function isRecord(value) {
606
- return typeof value === "object" && value !== null;
607
- }
608
- /** Exact-key check: `value` has every key in `keys` and nothing else. */
609
- function hasExactKeys(value, keys) {
610
- return Object.keys(value).length === keys.length && keys.every((k) => Object.hasOwn(value, k));
611
- }
612
- /**
613
- * Classify an event for packing: its delta kind when the ENTIRE shape
614
- * (envelope, data, chunk — exact keys, primitive types, integer seq/time) is
615
- * whitelisted, else `undefined` (store verbatim). Inputs come from live typed
616
- * appends AND parsed fixture files, so the checks are structural, not
617
- * type-trusted. Integer times keep gap encoding exact: a fractional time would
618
- * reconstruct through float subtraction/addition, which need not round-trip.
619
- */
620
- function classify(event) {
621
- if (event.type !== "assistant/chunk") return void 0;
622
- if (!hasExactKeys(event, [
623
- "type",
624
- "seq",
625
- "time",
626
- "data"
627
- ])) return void 0;
628
- if (!Number.isSafeInteger(event.seq) || event.seq < 0 || Object.is(event.seq, -0) || !Number.isSafeInteger(event.time)) return void 0;
629
- const data = event.data;
630
- if (!isRecord(data) || !hasExactKeys(data, [
631
- "turn",
632
- "step",
633
- "chunk"
634
- ])) return void 0;
635
- if (typeof data.turn !== "number" || typeof data.step !== "number") return void 0;
636
- const chunk = data.chunk;
637
- if (!isRecord(chunk) || typeof chunk.index !== "number") return void 0;
638
- switch (chunk.type) {
639
- case "text-delta":
640
- case "reasoning-delta": return hasExactKeys(chunk, [
641
- "type",
642
- "index",
643
- "text"
644
- ]) && typeof chunk.text === "string" ? chunk.type : void 0;
645
- case "tool-call-delta": return (hasExactKeys(chunk, [
646
- "type",
647
- "index",
648
- "id",
649
- "argumentsDelta"
650
- ]) || hasExactKeys(chunk, [
651
- "type",
652
- "index",
653
- "id",
654
- "name",
655
- "argumentsDelta"
656
- ]) && typeof chunk.name === "string") && typeof chunk.id === "string" && typeof chunk.argumentsDelta === "string" ? chunk.type : void 0;
657
- default: return;
658
- }
659
- }
660
- /** The tool-call fields of a whitelisted delta chunk (only after {@link classify} returned `'tool-call-delta'`). */
661
- function toolCallOf(event) {
662
- return event.data.chunk;
663
- }
664
- /** The block index of a whitelisted delta chunk (not every {@link StreamChunk} variant carries one). */
665
- function indexOf(event) {
666
- return event.data.chunk.index;
667
- }
668
- /** Whether `next` extends a run ending in `prev` (same kind already checked by the caller). */
669
- function continues(prev, next, kind) {
670
- if (next.seq !== prev.seq + 1) return false;
671
- if (!Number.isSafeInteger(next.time - prev.time)) return false;
672
- if (next.data.turn !== prev.data.turn || next.data.step !== prev.data.step) return false;
673
- if (indexOf(next) !== indexOf(prev)) return false;
674
- if (kind !== "tool-call-delta") return true;
675
- const a = toolCallOf(prev);
676
- const b = toolCallOf(next);
677
- return a.id === b.id && Object.hasOwn(a, "name") === Object.hasOwn(b, "name") && a.name === b.name;
678
- }
679
- /** Build the row for a completed run (`run.length >= MIN_RUN`, uniform per {@link continues}). */
680
- function buildRow(kind, run) {
681
- const first = run[0];
682
- const base = {
683
- turn: first.data.turn,
684
- step: first.data.step,
685
- index: indexOf(first),
686
- dt: run.slice(1).map((event, i) => event.time - run[i].time)
687
- };
688
- const envelope = {
689
- seq0: first.seq,
690
- time0: first.time
691
- };
692
- if (kind === "tool-call-delta") {
693
- const call = toolCallOf(first);
694
- return {
695
- type: "tool-call-chunks",
696
- ...envelope,
697
- data: {
698
- ...base,
699
- id: brandString(call.id),
700
- ...Object.hasOwn(call, "name") ? { name: call.name } : {},
701
- args: run.map((event) => event.data.chunk.argumentsDelta)
702
- }
703
- };
704
- }
705
- const data = {
706
- ...base,
707
- texts: run.map((event) => event.data.chunk.text)
708
- };
709
- return kind === "text-delta" ? {
710
- type: "text-chunks",
711
- ...envelope,
712
- data
713
- } : {
714
- type: "reasoning-chunks",
715
- ...envelope,
716
- data
717
- };
718
- }
719
- /**
720
- * Pack an event batch for storage: each run of at least {@link MIN_RUN}
721
- * consecutive whitelisted same-kind, same-block delta chunk events becomes one
722
- * {@link ChunkRow}; every other event passes through verbatim, in order.
723
- * Pure and stateless — safe over any array, including a batch whose runs were
724
- * split by flush boundaries (the split runs simply pack per batch).
725
- *
726
- * @param events - the batch to encode, in log order.
727
- * @returns the storage records to write, one JSONL line each.
728
- */
729
- function packChunkRuns(events) {
730
- const out = [];
731
- let kind;
732
- let run = [];
733
- const flush = () => {
734
- if (kind !== void 0 && run.length >= MIN_RUN) out.push(buildRow(kind, run));
735
- else out.push(...run);
736
- kind = void 0;
737
- run = [];
738
- };
739
- for (const event of events) {
740
- const k = classify(event);
741
- if (k === void 0) {
742
- flush();
743
- out.push(event);
744
- continue;
745
- }
746
- const delta = event;
747
- const last = run[run.length - 1];
748
- if (k === kind && last !== void 0 && continues(last, delta, k)) {
749
- run.push(delta);
750
- continue;
751
- }
752
- flush();
753
- kind = k;
754
- run = [delta];
755
- }
756
- flush();
757
- return out;
758
- }
759
- /** Throw the uniform malformed-row diagnostic. */
760
- function malformed(tag, why) {
761
- throw new Error(`malformed ${tag} storage row: ${why}`);
762
- }
763
- /** Validate the shared run-data fields and the payload/dt arity; returns the member payload. */
764
- function validateRunData(tag, data, payloadKey) {
765
- if (typeof data.turn !== "number" || typeof data.step !== "number" || typeof data.index !== "number") malformed(tag, "turn/step/index must be numbers");
766
- const payload = data[payloadKey];
767
- if (!Array.isArray(payload) || payload.length === 0 || payload.some((entry) => typeof entry !== "string")) malformed(tag, `${payloadKey} must be a non-empty string array`);
768
- const dt = data.dt;
769
- if (!Array.isArray(dt) || dt.some((gap) => !Number.isSafeInteger(gap))) malformed(tag, "dt must be an array of safe integers");
770
- if (dt.length !== payload.length - 1) malformed(tag, `dt length ${dt.length} does not match ${payload.length} members`);
771
- return payload;
772
- }
773
- /** Validate a row-tagged parsed value's envelope and data, throwing on any malformation. */
774
- function validateRow(value, tag) {
775
- if (!hasExactKeys(value, [
776
- "type",
777
- "seq0",
778
- "time0",
779
- "data"
780
- ])) malformed(tag, "envelope must be exactly {type, seq0, time0, data}");
781
- if (!Number.isSafeInteger(value.seq0) || value.seq0 < 0 || Object.is(value.seq0, -0)) malformed(tag, "seq0 must be a non-negative safe integer");
782
- if (!Number.isSafeInteger(value.time0)) malformed(tag, "time0 must be a safe integer");
783
- const data = value.data;
784
- if (!isRecord(data)) malformed(tag, "data must be an object");
785
- let payload;
786
- if (tag === "tool-call-chunks") {
787
- const withName = hasExactKeys(data, [
788
- "turn",
789
- "step",
790
- "index",
791
- "id",
792
- "name",
793
- "dt",
794
- "args"
795
- ]);
796
- if (!withName && !hasExactKeys(data, [
797
- "turn",
798
- "step",
799
- "index",
800
- "id",
801
- "dt",
802
- "args"
803
- ])) malformed(tag, "data must be exactly {turn, step, index, id, name?, dt, args}");
804
- if (typeof data.id !== "string" || withName && typeof data.name !== "string") malformed(tag, "id (and name when present) must be strings");
805
- payload = validateRunData(tag, data, "args");
806
- } else {
807
- if (!hasExactKeys(data, [
808
- "turn",
809
- "step",
810
- "index",
811
- "dt",
812
- "texts"
813
- ])) malformed(tag, "data must be exactly {turn, step, index, dt, texts}");
814
- payload = validateRunData(tag, data, "texts");
815
- }
816
- if (payload.length - 1 > Number.MAX_SAFE_INTEGER - value.seq0) malformed(tag, "member seqs must stay safe integers");
817
- let time = value.time0;
818
- for (const gap of data.dt) {
819
- time += gap;
820
- if (!Number.isSafeInteger(time)) malformed(tag, "member times must stay safe integers");
821
- }
822
- SessionSeq(value.seq0);
823
- return value;
824
- }
825
- /** Expand a validated row back into its exact original events, in order. */
826
- function expandRow(row) {
827
- const members = row.type === "tool-call-chunks" ? row.data.args : row.data.texts;
828
- const events = [];
829
- let time = row.time0;
830
- for (let k = 0; k < members.length; k++) {
831
- if (k > 0) time += row.data.dt[k - 1];
832
- let chunk;
833
- switch (row.type) {
834
- case "text-chunks":
835
- chunk = {
836
- type: "text-delta",
837
- index: row.data.index,
838
- text: members[k]
839
- };
840
- break;
841
- case "reasoning-chunks":
842
- chunk = {
843
- type: "reasoning-delta",
844
- index: row.data.index,
845
- text: members[k]
846
- };
847
- break;
848
- case "tool-call-chunks":
849
- chunk = {
850
- type: "tool-call-delta",
851
- index: row.data.index,
852
- id: row.data.id,
853
- ...Object.hasOwn(row.data, "name") ? { name: row.data.name } : {},
854
- argumentsDelta: members[k]
855
- };
856
- break;
857
- /* v8 ignore next 4 -- validateRow only returns the three row tags */
858
- default: throw new Error(`chunk-rows received unsupported row ${String(row)}`);
859
- }
860
- events.push({
861
- type: "assistant/chunk",
862
- seq: SessionSeq(row.seq0 + k),
863
- time,
864
- data: {
865
- turn: row.data.turn,
866
- step: row.data.step,
867
- chunk
868
- }
869
- });
870
- }
871
- return events;
872
- }
873
- /**
874
- * Decode one parsed JSONL line value into the session event(s) it stores.
875
- * Chunk-row-tagged values validate and expand (a malformed row throws — it is
876
- * corrupt storage, and treating it as an event would silently drop a whole
877
- * run); every other value passes through as a single event after admitting a
878
- * numeric `seq` through the Session-sequence constructor.
879
- *
880
- * @param value - one line's `JSON.parse` result.
881
- * @returns the stored events, in log order.
882
- */
883
- function decodeStorageRecord(value) {
884
- if (!isRecord(value)) return [value];
885
- const tag = value.type;
886
- if (tag !== "text-chunks" && tag !== "reasoning-chunks" && tag !== "tool-call-chunks") {
887
- if (typeof value.seq === "number") SessionSeq(value.seq);
888
- return [value];
889
- }
890
- return expandRow(validateRow(value, tag));
891
- }
892
- //#endregion
893
- //#region lib/types/known-event-types.js
894
- /**
895
- * GENERATED by `scripts/gen-persistence-catalog.ts` — do not edit by hand; run
896
- * `pnpm run gen-persistence-catalog` to regenerate (verified fresh by
897
- * `pnpm run verify-persistence-catalog`, part of `doc-sync`).
898
- * @module @deepseek-ai/dsh-session/known-event-types
899
- */
900
- /**
901
- * Every `SessionEventMap` member declared in this repository — the event
902
- * vocabulary this build understands. The persistence read path refuses to
903
- * interpret a log containing a type outside this set unless the event
904
- * carries the envelope's `ignorable` marker (see `SessionEvent.ignorable`
905
- * in `./types.ts`): such a log was likely written by a newer harness, and
906
- * silently skipping a required event would reconstruct a wrong session.
907
- * Downstream (out-of-repo) plugin events are outside this list by
908
- * construction. The persisted `SessionEvent.ignorable` marker is the
909
- * compatibility mechanism; event-name registration was rejected because
910
- * it does not classify omission safety and would make reads
911
- * composition-dependent. The rationale is in
912
- * `.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.md`.
913
- */
914
- const KNOWN_SESSION_EVENT_TYPES = new Set([
915
- "agent-preset/selected",
916
- "agent/inbox/spliced",
917
- "approval/asked",
918
- "approval/decided",
919
- "approval/policy",
920
- "assistant/chunk",
921
- "assistant/message",
922
- "command/done",
923
- "command/run",
924
- "compaction/end",
925
- "compaction/prune",
926
- "compaction/start",
927
- "compaction/summary",
928
- "feedback/record",
929
- "goal/change",
930
- "hook/invoked",
931
- "hook/result",
932
- "llm/retry",
933
- "llm/retry-started",
934
- "model/selection",
935
- "permission/preset",
936
- "plan/mode",
937
- "request/context",
938
- "request/header",
939
- "sandbox/mode",
940
- "schedule/change",
941
- "session-log-deepseek/delivery-accepted",
942
- "session/end-seed",
943
- "session/title",
944
- "session/title-llm-request",
945
- "step/end",
946
- "step/start",
947
- "subagent/descriptor",
948
- "subagent/model-selection-policy",
949
- "team/member",
950
- "team/message/delivered",
951
- "team/message/queued",
952
- "team/task",
953
- "todo/write",
954
- "tool-workflow/agent-end",
955
- "tool-workflow/agent-start",
956
- "tool-workflow/run-end",
957
- "tool-workflow/run-start",
958
- "tool/call",
959
- "tool/code-dispatch",
960
- "tool/code-dispatch-start",
961
- "tool/result",
962
- "turn/end",
963
- "turn/start",
964
- "user/message",
965
- "web/deepseek-search-llm-request"
966
- ]);
967
- //#endregion
968
710
  //#region lib/types/seq-ranges.js
969
711
  /** Lossless range encoding for JSONL `sourceEventSeqs` arrays. */
970
712
  function isStrictlyIncreasing(values) {
@@ -1034,7 +776,7 @@ function validateSessionHeader(id, input) {
1034
776
  if (input === null || typeof input !== "object" || Array.isArray(input)) throw new Error("session header is not a plain JSON record");
1035
777
  const record = input;
1036
778
  if (Object.hasOwn(record, "seedLength")) throw new Error("session header has invalid field \"seedLength\"");
1037
- if (record.version !== 0) throw new Error(`session header version must be 0, got ${String(record.version)}`);
779
+ if (record.version !== 3) throw new Error(`session header version must be 3, got ${String(record.version)}`);
1038
780
  if (record.id !== id) throw new Error(`session header id "${String(record.id)}" does not match session id "${id}"`);
1039
781
  if (typeof record.createdAt !== "number" || !Number.isSafeInteger(record.createdAt) || record.createdAt < 0) throw new Error("session header createdAt must be a non-negative safe integer");
1040
782
  if (record.cwd !== void 0) {
@@ -1059,7 +801,7 @@ function validateRestoredSessionHeader(id, input) {
1059
801
  /** Detach, validate, and freeze the creation metadata published by a session. */
1060
802
  function snapshotSessionHeader(id, source) {
1061
803
  const snapshot = snapshotJsonValue(source === void 0 ? {
1062
- version: 0,
804
+ version: 3,
1063
805
  id,
1064
806
  createdAt: Date.now(),
1065
807
  isSeeded: false
@@ -1074,13 +816,17 @@ function snapshotSessionHeader(id, source) {
1074
816
  * Use {@link snapshotSessionEvent} when exclusive ownership is not guaranteed.
1075
817
  * @param event - exclusively owned event imported across a trusted boundary.
1076
818
  * @returns the same event object with a validated, deeply frozen message.
819
+ * @throws when event-local surface metadata, request-header fields, or message invariants are invalid; history relations are not checked.
1077
820
  */
1078
821
  function adoptSessionEvent(event) {
822
+ validateSessionEventData(event, `session event at seq ${event.seq}`);
823
+ validateSurfaceMetadata(event);
1079
824
  assertMessageEventShape(event, `session event at seq ${event.seq}`);
1080
825
  switch (event.type) {
1081
826
  case "user/message":
1082
827
  deepFreeze(event.data);
1083
828
  break;
829
+ case "system/message":
1084
830
  case "assistant/message":
1085
831
  case "tool/result":
1086
832
  deepFreeze(event.data.message);
@@ -1097,23 +843,10 @@ function adoptSessionEvent(event) {
1097
843
  function snapshotSessionEvent(event) {
1098
844
  return adoptSessionEvent(structuredClone(event));
1099
845
  }
1100
- /** Deep-freeze one acyclic JSON tree without consuming the JavaScript call stack. */
1101
- function freezeRestoredObject(value) {
1102
- const pending = [value];
1103
- while (pending.length > 0) {
1104
- const current = pending.pop();
1105
- Object.freeze(current);
1106
- for (const key in current) {
1107
- const child = current[key];
1108
- if (child !== null && typeof child === "object") pending.push(child);
1109
- }
1110
- }
1111
- return value;
1112
- }
1113
846
  /** Validate the fixed event envelope after one-pass JSON materialization. */
1114
847
  function assertSessionEventEnvelope(value, index) {
848
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`seed event at index ${index} has an invalid event envelope`);
1115
849
  const event = value;
1116
- if (event["type"] === "request/header-delta") throw new Error(`seed event at index ${index} uses unsupported legacy request/header-delta format`);
1117
850
  for (const key in event) switch (key) {
1118
851
  case "type":
1119
852
  case "seq":
@@ -1128,9 +861,12 @@ function assertSessionEventEnvelope(value, index) {
1128
861
  const seq = event["seq"];
1129
862
  const time = event["time"];
1130
863
  if (typeof type !== "string" || typeof seq !== "number" || !Number.isSafeInteger(seq) || seq < 0 || Object.is(seq, -0) || typeof time !== "number" || !Number.isSafeInteger(time) || event["data"] === void 0 || event["ignorable"] !== void 0 && event["ignorable"] !== true) throw new Error(`seed event at index ${index} has an invalid event envelope`);
864
+ validateSessionEventData(event, `seed ${type} at index ${index}`);
1131
865
  switch (type) {
1132
866
  case "request/header":
867
+ case "system/message":
1133
868
  case "user/message":
869
+ case "assistant/attempt":
1134
870
  case "assistant/message":
1135
871
  case "tool/result":
1136
872
  assertCurrentLlmShape(event, index);
@@ -1142,18 +878,31 @@ function assertCurrentLlmShape(event, index) {
1142
878
  const data = event["data"];
1143
879
  const record = typeof data === "object" && data !== null ? data : void 0;
1144
880
  if (event["type"] === "request/header") {
1145
- const header = record?.["header"];
1146
- const headerRecord = typeof header === "object" && header !== null && !Array.isArray(header) ? header : void 0;
1147
- const config = headerRecord?.["config"];
881
+ const headerRecord = record?.["header"];
882
+ const config = headerRecord["config"];
1148
883
  if (!hasProviderModel(config)) throw new Error(`seed request/header at index ${index} lacks provider/model`);
1149
884
  const configRecord = config;
1150
885
  const reasoningEffort = configRecord["reasoningEffort"];
1151
886
  if (reasoningEffort !== void 0 && (typeof reasoningEffort !== "string" || reasoningEffort.length === 0)) throw new Error(`seed request/header at index ${index} has an invalid reasoningEffort`);
1152
- assertAdapterDefaults(headerRecord?.["adapterDefaults"], configRecord, index);
887
+ assertAdapterDefaults(headerRecord["adapterDefaults"], configRecord, index);
888
+ const reason = record?.["reason"];
889
+ if (reason !== "initial" && reason !== "resume" && reason !== "change" && reason !== "series") throw new Error(`seed request/header at index ${index} has an invalid reason`);
890
+ if (record?.["startsSeries"] !== void 0 && record["startsSeries"] !== true) throw new Error(`seed request/header at index ${index} has an invalid startsSeries marker`);
1153
891
  }
1154
892
  const type = event["type"];
1155
- if (type !== "user/message" && type !== "assistant/message" && type !== "tool/result") return;
893
+ if (type === "assistant/attempt") {
894
+ assertAssistantSettlementShape(record, type, index);
895
+ return;
896
+ }
897
+ if (!isMessageEventType(type)) return;
1156
898
  assertMessageEventShape(event, `seed ${type} at index ${index}`);
899
+ if (type === "assistant/message") assertAssistantSettlementShape(record, type, index);
900
+ }
901
+ /** Validate fields used directly by restored Session lifecycle logic without replaying the embedded stream. */
902
+ function assertAssistantSettlementShape(data, type, index) {
903
+ const turn = data?.["turn"];
904
+ const step = data?.["step"];
905
+ if (typeof turn !== "number" || !Number.isSafeInteger(turn) || turn < 0 || Object.is(turn, -0) || typeof step !== "number" || !Number.isSafeInteger(step) || step < 0 || Object.is(step, -0) || !Array.isArray(data?.["stream"])) throw new Error(`seed ${type} at index ${index} has invalid settlement fields`);
1157
906
  }
1158
907
  const allowedAdapterKeys = new Set(["reasoningEffort", "maxTokens"]);
1159
908
  /** Validate adapter-default markers imported from a durable request header. */
@@ -1163,21 +912,35 @@ function assertAdapterDefaults(value, config, index) {
1163
912
  const defaults = value;
1164
913
  if (Object.keys(defaults).some((key) => !allowedAdapterKeys.has(key)) || Object.values(defaults).some((marker) => marker !== true) || defaults["reasoningEffort"] === true && config["reasoningEffort"] === void 0 || defaults["maxTokens"] === true && config["maxTokens"] === void 0) throw new Error(`seed request/header at index ${index} has invalid adapterDefaults`);
1165
914
  }
915
+ /** The four surface event types whose payload carries an identified message. */
916
+ function isMessageEventType(type) {
917
+ return type === "system/message" || type === "user/message" || type === "assistant/message" || type === "tool/result";
918
+ }
919
+ const MESSAGE_ROLE_BY_TYPE = {
920
+ "system/message": "system",
921
+ "user/message": "user",
922
+ "assistant/message": "assistant",
923
+ "tool/result": "user"
924
+ };
1166
925
  /** Validate only the event-specific invariants needed to safely replay a message. */
1167
926
  function assertMessageEventShape(event, subject) {
1168
927
  const type = event["type"];
1169
- if (type !== "user/message" && type !== "assistant/message" && type !== "tool/result") return;
928
+ if (!isMessageEventType(type)) return;
1170
929
  const data = event["data"];
1171
930
  const record = typeof data === "object" && data !== null ? data : void 0;
1172
931
  const message = type === "user/message" ? record : record?.["message"];
1173
932
  if (typeof message !== "object" || message === null || typeof message["id"] !== "string" || message["id"] === "") throw new Error(`${subject} lacks an identified message`);
1174
933
  const messageRecord = message;
1175
- const expectedRole = type === "assistant/message" ? "assistant" : "user";
934
+ const expectedRole = MESSAGE_ROLE_BY_TYPE[type];
1176
935
  if (messageRecord["role"] !== expectedRole) throw new Error(`${subject} message must have role "${expectedRole}"`);
1177
936
  const source = messageRecord["source"];
1178
937
  if (typeof source !== "object" || source === null || typeof source["kind"] !== "string" || source["kind"] === "") throw new Error(`${subject} message has invalid source`);
1179
938
  if (!Array.isArray(messageRecord["content"])) throw new Error(`${subject} message has invalid content`);
1180
939
  const sourceRecord = source;
940
+ if (type === "system/message") {
941
+ if (sourceRecord["kind"] !== "plugin" || typeof sourceRecord["plugin"] !== "string" || sourceRecord["plugin"] === "") throw new Error(`${subject} message must have plugin source`);
942
+ return;
943
+ }
1181
944
  if (type === "assistant/message") {
1182
945
  if (sourceRecord["kind"] !== "model" || !hasProviderModel(sourceRecord)) throw new Error(`${subject} message must have model source`);
1183
946
  return;
@@ -1195,11 +958,6 @@ function hasProviderModel(value) {
1195
958
  const pair = value;
1196
959
  return typeof pair["provider"] === "string" && pair["provider"].length > 0 && typeof pair["model"] === "string" && pair["model"].length > 0;
1197
960
  }
1198
- /** Reject request-header vocabulary removed with the legacy delta codec. */
1199
- function assertSupportedRequestHeader(type, data, location) {
1200
- if (type === "request/header-delta") throw new Error(`${location} uses unsupported legacy request/header-delta format`);
1201
- if (type === "request/header" && data !== null && typeof data === "object" && !Array.isArray(data) && data["reason"] === "fallback") throw new Error(`${location} uses unsupported legacy request/header reason "fallback"`);
1202
- }
1203
961
  /** Resolve one listener snapshot, including Cordis's internal dispatch checks. */
1204
962
  function collectSessionCallbacks(ctx, args) {
1205
963
  return [...ctx.events.dispatch("emit", args)];
@@ -1252,9 +1010,10 @@ var Session = class Session {
1252
1010
  * The first seq appended IN THIS PROCESS: the length of the constructor
1253
1011
  * seed (0 without one). Events with smaller seq values entered through
1254
1012
  * construction — replay, fork, or resume — and were never published on the
1255
- * `session/event` firehose (constructor seeds do not emit), so consumers
1256
- * that replay the log as a publication substitute (telemetry adoption)
1257
- * start here. Distinct from {@link inheritedEventCount}, the DURABLE
1013
+ * `session/event` firehose (constructor seeds do not emit). This offset marks
1014
+ * the constructor-input boundary for lifecycle ownership and persistence
1015
+ * adoption; consumers that need complete canonical history still start at
1016
+ * seq 0. Distinct from {@link inheritedEventCount}, the DURABLE
1258
1017
  * fork-lineage cut: a resumed session's constructor seed is its full stored
1259
1018
  * log, while the inherited count keeps the original fork value — this field is the
1260
1019
  * in-process construction fact.
@@ -1284,32 +1043,34 @@ var Session = class Session {
1284
1043
  return new Session(id, seed, header, "snapshot", inheritedEventCount);
1285
1044
  }
1286
1045
  /**
1287
- * Restore a detached session by taking ownership of fresh persistence values.
1288
- * The storage format, event envelopes, sequence continuity, surface transitions,
1289
- * and header fields are validated before the restored objects are frozen.
1046
+ * Restore a detached session by adopting an independently owned or deeply frozen seed.
1047
+ * Runtime-required event fields, event envelopes, sequence continuity, surface
1048
+ * transitions, and header fields are validated without copying or freezing events.
1049
+ * Embedded Assistant streams remain opaque until a stream consumer or storage
1050
+ * verifier reads them.
1290
1051
  * @param id - restored session identity.
1291
- * @param seed - fresh detached events whose ownership is transferred.
1292
- * @param header - fresh detached metadata whose ownership is transferred.
1052
+ * @param seed - independently owned or deeply frozen events.
1053
+ * @param header - independently owned storage metadata.
1293
1054
  * @param inheritedEventCount - exact fork-inherited prefix length decoded from storage.
1055
+ * @param eventState - aliasing state carried from the operation that produced the seed.
1294
1056
  * @returns a restored detached session.
1295
1057
  */
1296
- static fromRestore(id, seed, header, inheritedEventCount) {
1297
- return new Session(id, seed, header, "restore", inheritedEventCount);
1058
+ static fromRestore(id, seed, header, inheritedEventCount, eventState) {
1059
+ return new Session(id, seed, header, eventState, inheritedEventCount);
1298
1060
  }
1299
1061
  constructor(id, seed, header, mode = "snapshot", suppliedInheritedEventCount) {
1300
- const restoredHeader = mode === "restore" ? validateRestoredSessionHeader(id, header) : void 0;
1062
+ const restoredHeader = mode === "snapshot" ? void 0 : validateRestoredSessionHeader(id, header);
1301
1063
  if (seed !== void 0) for (const [index, source] of seed.entries()) {
1302
- const snapshot = mode === "restore" ? source : snapshotJsonValue(source);
1064
+ const snapshot = mode === "snapshot" ? snapshotJsonValue(source) : source;
1303
1065
  if (snapshot === void 0) throw new Error(`seed event at index ${index} is not losslessly JSON-serializable`);
1304
1066
  assertSessionEventEnvelope(snapshot, index);
1305
- assertSupportedRequestHeader(snapshot.type, snapshot.data, `seed event at index ${index}`);
1306
1067
  if (snapshot.seq !== index) throw new Error(`seed event at index ${index} has seq ${snapshot.seq} (expected ${index}); seed must be contiguous from 0`);
1307
1068
  try {
1308
1069
  this.surfaceManager.validateNext(snapshot);
1309
1070
  } catch (error) {
1310
1071
  throw new Error(`invalid seed event at index ${index}: ${error instanceof Error ? error.message : "invalid surface metadata"}`);
1311
1072
  }
1312
- this.log.push(mode === "restore" ? freezeRestoredObject(snapshot) : deepFreeze(snapshot));
1073
+ this.log.push(mode === "snapshot" ? deepFreeze(snapshot) : snapshot);
1313
1074
  }
1314
1075
  this.firstLiveSeq = SessionLogOffset(this.log.length);
1315
1076
  this.header = restoredHeader ?? snapshotSessionHeader(id, header);
@@ -1318,8 +1079,10 @@ var Session = class Session {
1318
1079
  const inheritedEventCount = SessionLogOffset(suppliedInheritedEventCount ?? 0);
1319
1080
  if (!this.header.isSeeded && inheritedEventCount !== 0) throw new Error("unseeded session inherited event count must be 0");
1320
1081
  if (inheritedEventCount > this.log.length) throw new Error("session inherited event count exceeds its event log");
1082
+ if (mode === "snapshot" && this.header.isSeeded && inheritedEventCount !== this.log.length) throw new Error("seeded session constructor seed must equal its inherited prefix");
1321
1083
  this.inheritedEventCount = inheritedEventCount;
1322
- if (seed !== void 0 && this.log.at(-1)?.type !== "session/end-seed") this.append("session/end-seed", {});
1084
+ if (seed !== void 0 && mode === "snapshot" && this.header.isSeeded) this.append("session/end-seed", { inherited: true });
1085
+ else if (seed !== void 0 && this.log.at(-1)?.type !== "session/end-seed") this.append("session/end-seed", {});
1323
1086
  }
1324
1087
  /** Cached immutable full snapshot of the private append-only log. */
1325
1088
  eventsSnapshot;
@@ -1382,7 +1145,8 @@ var Session = class Session {
1382
1145
  * declare how it joins the surface, the sole source of derived model
1383
1146
  * history) and
1384
1147
  * rejected by the compiler for non-surface types like `turn/start` or
1385
- * `assistant/chunk`.
1148
+ * `assistant/attempt`. Assistant messages embed their exact provider
1149
+ * stream and cannot cite top-level source events.
1386
1150
  * @returns the logged event — its assigned `seq`/`time` plus the SNAPSHOT of
1387
1151
  * `data` that entered the log, so reading `event.data` back sees the logged
1388
1152
  * value, never the caller's still-mutable input.
@@ -1390,6 +1154,7 @@ var Session = class Session {
1390
1154
  * (BigInt, function, symbol, undefined, negative zero, non-finite number,
1391
1155
  * circular reference, sparse array, or an exotic object such as
1392
1156
  * Map/Set/Date/class instance), or when the candidate violates the
1157
+ * request-header empty-field or tool-error consistency rules, or the
1393
1158
  * canonical surface contract (marker shape and eligibility, unique
1394
1159
  * earlier source-event references, positional replacement validity, and complete
1395
1160
  * shadowed-node coverage). One iterative pass reads, validates, and
@@ -1408,7 +1173,6 @@ var Session = class Session {
1408
1173
  };
1409
1174
  const dataSnapshot = snapshotJsonValue(data);
1410
1175
  if (dataSnapshot === void 0) throw new Error(`session event "${type}" carries non-JSON-serializable data`);
1411
- assertSupportedRequestHeader(type, dataSnapshot, `session event "${type}"`);
1412
1176
  const surfaceMetadataSnapshot = snapshotJsonValue(surfaceMetadata);
1413
1177
  if (surfaceMetadataSnapshot === void 0) throw new Error(`session event "${type}" carries non-JSON-serializable surface metadata`);
1414
1178
  const entry = attachments.get(this);
@@ -1420,6 +1184,7 @@ var Session = class Session {
1420
1184
  data: dataSnapshot,
1421
1185
  ...surfaceMetadataSnapshot
1422
1186
  });
1187
+ validateSessionEventData(event, `session event "${type}" at seq ${event.seq}`);
1423
1188
  this.surfaceManager.validateNext(event);
1424
1189
  if (entry !== void 0) entry.appending = true;
1425
1190
  try {
@@ -1537,8 +1302,9 @@ var SessionForkError = class extends Error {
1537
1302
  /**
1538
1303
  * In-memory session store (`ctx.sessions`).
1539
1304
  *
1540
- * Persistence is intentionally not implemented here — persistence plugins
1541
- * subscribe to `session/event` and flush on `session/flush` / dispose.
1305
+ * Persistence is intentionally not implemented here — the agent lifecycle
1306
+ * attaches a session-log writer to each published session's write handle;
1307
+ * a session published outside that lifecycle persists nothing.
1542
1308
  */
1543
1309
  var SessionStore = class extends Service {
1544
1310
  store = /* @__PURE__ */ new Map();
@@ -1595,10 +1361,9 @@ var SessionStore = class extends Service {
1595
1361
  *
1596
1362
  * @param id - the session id; omitted, the store mints `session-<n>`.
1597
1363
  * @param options - seed events and/or creation metadata for the header. With
1598
- * `seedSource: 'persistence'`, metadata and events must be fresh detached
1599
- * graphs whose ownership transfers to this call: they are validated and
1600
- * frozen in place through {@link Session.fromRestore}, so the caller must
1601
- * retain no mutable aliases.
1364
+ * `eventState`, every seed event is either independently owned or any
1365
+ * shared value is deeply frozen; {@link Session.fromRestore} validates and
1366
+ * adopts those values without copying or freezing them.
1602
1367
  * @returns the constructed session, NOT yet in the store.
1603
1368
  * @throws if a session with `id` already exists, metadata is not a plain
1604
1369
  * lossless-JSON record with valid scalar fields, or `meta.cwd` is a
@@ -1611,11 +1376,20 @@ var SessionStore = class extends Service {
1611
1376
  while (this.store.has(sessionId));
1612
1377
  else sessionId = brandString(id);
1613
1378
  if (this.store.has(sessionId)) throw new Error(`session "${sessionId}" already exists`);
1614
- if (options?.seedSource === "persistence") return Session.fromRestore(sessionId, options.seed, options.meta, options.inheritedEventCount);
1379
+ if (options !== void 0) {
1380
+ const { eventState } = options;
1381
+ switch (eventState) {
1382
+ case "detached":
1383
+ case "shared-frozen": return Session.fromRestore(sessionId, options.seed, options.meta, options.inheritedEventCount, eventState);
1384
+ case void 0: break;
1385
+ /* v8 ignore next -- closed-union exhaustiveness guard */
1386
+ default: assertNever(eventState, "SessionStore.prepare event state");
1387
+ }
1388
+ }
1615
1389
  const seed = options?.seed;
1616
1390
  const meta = options?.meta;
1617
1391
  const header = {
1618
- version: 0,
1392
+ version: 3,
1619
1393
  id: sessionId,
1620
1394
  createdAt: meta?.createdAt ?? Date.now(),
1621
1395
  ...meta?.cwd === void 0 ? {} : { cwd: meta.cwd },
@@ -1847,4 +1621,4 @@ var SessionStore = class extends Service {
1847
1621
  }
1848
1622
  };
1849
1623
  //#endregion
1850
- export { KNOWN_SESSION_EVENT_TYPES, SESSION_FORMAT_VERSION, Session, SessionForkError, SessionId, SessionLogOffset, SessionPreparation, SessionSeq, SessionStore, SessionStore as default, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN, adoptSessionEvent, canonicalHeader, decodeSeqRanges, decodeStorageRecord, deriveEventMessage, encodeSeqRanges, foldRequestHeader, foldSurface, headerEquals, interruptedTurnClosers, isAppendSurfaceEvent, isReplacementSurfaceEvent, isSurfaceEligibleType, isSurfaceEvent, packChunkRuns, snapshotSessionEvent };
1624
+ export { KNOWN_SESSION_EVENT_TYPES, SESSION_FORMAT_VERSION, Session, SessionForkError, SessionId, SessionLogOffset, SessionPreparation, SessionSeq, SessionStore, SessionStore as default, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN, adoptSessionEvent, canonicalHeader, decodeSeqRanges, deriveEventMessage, encodeSeqRanges, foldRequestHeader, foldSurface, headerEquals, interruptedTurnClosers, isAppendSurfaceEvent, isReplacementSurfaceEvent, isSurfaceEligibleType, isSurfaceEvent, snapshotSessionEvent };