@deepseek-ai/dsh-session 0.1.3-alpha.2 → 0.1.5-alpha.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.
package/lib/index.js CHANGED
@@ -53,7 +53,87 @@ function SessionLogOffset(value) {
53
53
  * immutable prior-generation, and current fast-path rules are recorded in
54
54
  * `.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md`.
55
55
  */
56
- const SESSION_FORMAT_VERSION = 2;
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
+ "deliverables/presented",
94
+ "feedback/message-delete",
95
+ "feedback/message-put",
96
+ "feedback/record",
97
+ "goal/change",
98
+ "hook/invoked",
99
+ "hook/result",
100
+ "llm/retry",
101
+ "llm/retry-started",
102
+ "model/selection",
103
+ "permission/preset",
104
+ "plan/mode",
105
+ "request/context",
106
+ "request/header",
107
+ "sandbox/mode",
108
+ "schedule/change",
109
+ "session-log-deepseek/delivery-accepted",
110
+ "session/end-seed",
111
+ "session/title",
112
+ "session/title-llm-request",
113
+ "step/end",
114
+ "step/start",
115
+ "subagent/catalog",
116
+ "subagent/descriptor",
117
+ "subagent/model-selection-policy",
118
+ "system/message",
119
+ "team/member",
120
+ "team/message/delivered",
121
+ "team/message/queued",
122
+ "team/task",
123
+ "todo/write",
124
+ "tool-workflow/agent-end",
125
+ "tool-workflow/agent-start",
126
+ "tool-workflow/run-end",
127
+ "tool-workflow/run-start",
128
+ "tool/call",
129
+ "tool/ptc-dispatch",
130
+ "tool/ptc-dispatch-start",
131
+ "tool/result",
132
+ "turn/end",
133
+ "turn/start",
134
+ "user/message",
135
+ "web/deepseek-search-llm-request"
136
+ ]);
57
137
  //#endregion
58
138
  //#region lib/types/surface.js
59
139
  /**
@@ -67,6 +147,7 @@ const SESSION_FORMAT_VERSION = 2;
67
147
  */
68
148
  /** Runtime counterpart of the message-producing event union. */
69
149
  const SURFACE_EVENT_TYPES = new Set([
150
+ "system/message",
70
151
  "user/message",
71
152
  "assistant/message",
72
153
  "tool/result"
@@ -74,7 +155,7 @@ const SURFACE_EVENT_TYPES = new Set([
74
155
  /**
75
156
  * Whether an event type can join the model-visible surface.
76
157
  * @param type - event type to test.
77
- * @returns true for one of the three message-producing event types.
158
+ * @returns true for one of the four message-producing event types.
78
159
  */
79
160
  function isSurfaceEligibleType(type) {
80
161
  return SURFACE_EVENT_TYPES.has(type);
@@ -128,6 +209,7 @@ function isReplacementSurfaceEvent(event) {
128
209
  function deriveEventMessage(event) {
129
210
  switch (event.type) {
130
211
  case "user/message": return event.data;
212
+ case "system/message":
131
213
  case "assistant/message":
132
214
  if (event.data.message.content.length === 0) return null;
133
215
  return event.data.message;
@@ -135,6 +217,36 @@ function deriveEventMessage(event) {
135
217
  default: return null;
136
218
  }
137
219
  }
220
+ /** Whether a payload field is a JSON object rather than an array or scalar. */
221
+ function isRecord(value) {
222
+ return typeof value === "object" && value !== null && !Array.isArray(value);
223
+ }
224
+ /**
225
+ * Reject noncanonical request-header fields and contradictory tool failure metadata.
226
+ * This does not validate complete event payloads or embedded provider streams.
227
+ * @param event - event whose locally related payload fields are inspected.
228
+ * @param subject - event location to include in validation errors.
229
+ * @throws when request data/header is not an object, optional header fields are empty, or tool failure metadata contradicts its message.
230
+ */
231
+ function validateSessionEventData(event, subject) {
232
+ const data = event.data;
233
+ if (event.type === "request/header") {
234
+ if (!isRecord(data)) throw new Error(`${subject} data must be an object`);
235
+ const header = data["header"];
236
+ if (!isRecord(header)) throw new Error(`${subject} header must be an object`);
237
+ if (Object.hasOwn(header, "system")) throw new Error(`${subject} must omit header.system; use system/message`);
238
+ if (Array.isArray(header["tools"]) && header["tools"].length === 0) throw new Error(`${subject} must omit empty tools`);
239
+ const defaults = header["adapterDefaults"];
240
+ if (isRecord(defaults) && Object.keys(defaults).length === 0) throw new Error(`${subject} must omit empty adapterDefaults`);
241
+ } else if (event.type === "tool/result") {
242
+ if (!isRecord(data)) throw new Error(`${subject} data must be an object`);
243
+ if (data["error"] === void 0) return;
244
+ const message = data["message"];
245
+ const content = isRecord(message) ? message["content"] : void 0;
246
+ const block = Array.isArray(content) ? content[0] : void 0;
247
+ if (!isRecord(block) || block["isError"] !== true) throw new Error(`${subject} error requires message content[0].isError === true`);
248
+ }
249
+ }
138
250
  /** Create an empty surface fold state. */
139
251
  function createFoldState() {
140
252
  return {
@@ -149,12 +261,13 @@ function isEventSeq(value) {
149
261
  /** Whether a runtime value is the exact positional-replacement shape. */
150
262
  function isReplaceOp(value) {
151
263
  const op = value;
152
- 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"]);
264
+ 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"]);
153
265
  }
154
266
  /** Validate event-local surface eligibility and return its operation. */
155
267
  function surfaceOpOf(event) {
156
268
  const raw = event;
157
269
  if (!isSurfaceEligibleType(event.type)) {
270
+ if (!KNOWN_SESSION_EVENT_TYPES.has(event.type) && event.ignorable === true) return;
158
271
  if (raw.surfaceOp !== void 0) throw new Error(`session event "${event.type}" is not surface-eligible and cannot carry surfaceOp`);
159
272
  if (raw.sourceEventSeqs !== void 0) throw new Error(`session event "${event.type}" is not surface-eligible and cannot carry sourceEventSeqs`);
160
273
  return;
@@ -186,13 +299,26 @@ function assertProvenance(event, shadowedSeqs) {
186
299
  const missing = shadowedSeqs.filter((seq) => !sources.has(seq));
187
300
  if (missing.length > 0) throw new Error(`surface replace: sourceEventSeqs must include every shadowed surface node; missing ${missing.join(", ")}`);
188
301
  }
302
+ /**
303
+ * Validate one event's surface metadata without checking membership in a log or surface.
304
+ * @param event - event whose marker and source sequence values are inspected.
305
+ * Unknown ignorable records retain opaque metadata and never change the surface.
306
+ * @returns the validated operation, or undefined for a log-only or unknown ignorable event.
307
+ * @throws when metadata violates event-local eligibility, marker, or source-sequence rules.
308
+ */
309
+ function validateSurfaceMetadata(event) {
310
+ const op = surfaceOpOf(event);
311
+ 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`);
312
+ if (op !== void 0) assertProvenance(event, []);
313
+ return op;
314
+ }
189
315
  /** Locate one replacement range without mutating the current fold state. */
190
316
  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})`);
317
+ const startIdx = state.nodes.indexOf(op.startSeq);
318
+ if (startIdx === -1) throw new Error(`surface replace: start seq ${op.startSeq} not found in surface`);
319
+ const endIdx = state.nodes.indexOf(op.endSeq);
320
+ if (endIdx === -1) throw new Error(`surface replace: end seq ${op.endSeq} not found in surface`);
321
+ if (startIdx > endIdx) throw new Error(`surface replace: start seq ${op.startSeq} (index ${startIdx}) is after end seq ${op.endSeq} (index ${endIdx})`);
196
322
  return {
197
323
  startIdx,
198
324
  endIdx,
@@ -244,26 +370,35 @@ function assertToolResultRewrite(event, shadowedSeqs, events, baseSeq) {
244
370
  if (!isDeepEqualJson(originalRest, replacementRest)) throw new Error("tool/result surface replacement may change only content");
245
371
  }
246
372
  }
373
+ /**
374
+ * Protect the system prompt at surface node 0. A replacement covering node 0
375
+ * while that node is a `system/message` must itself be a `system/message` over
376
+ * exactly that node; later system nodes carry no protection and a compaction
377
+ * range may shadow them.
378
+ */
379
+ function assertSystemHeadRewrite(event, state, startIdx, shadowedSeqs, events, baseSeq) {
380
+ if (startIdx !== 0) return;
381
+ if (events[state.nodes[0] - baseSeq]?.type !== "system/message") return;
382
+ 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");
383
+ }
247
384
  /** Validate one event at its replay boundary and prepare its atomic fold transition. */
248
385
  function planSurfaceEvent(state, event, expectedSeq, events, baseSeq) {
249
386
  if (event.seq !== expectedSeq) throw new Error(`session event seq ${event.seq} is not contiguous; expected ${expectedSeq}`);
250
- const surfaceOp = surfaceOpOf(event);
387
+ const surfaceOp = validateSurfaceMetadata(event);
251
388
  if (surfaceOp === void 0) return;
252
- if (surfaceOp === "append") {
253
- assertProvenance(event, []);
254
- return {
255
- kind: "append",
256
- seq: event.seq
257
- };
258
- }
389
+ if (surfaceOp === "append") return {
390
+ kind: "append",
391
+ seq: event.seq
392
+ };
259
393
  const range = replacementRange(state, surfaceOp);
260
394
  assertProvenance(event, range.shadowedSeqs);
261
395
  assertToolResultRewrite(event, range.shadowedSeqs, events, baseSeq);
396
+ assertSystemHeadRewrite(event, state, range.startIdx, range.shadowedSeqs, events, baseSeq);
262
397
  return {
263
398
  kind: "replace",
264
399
  seq: event.seq,
265
- start: surfaceOp.start,
266
- end: surfaceOp.end,
400
+ start: surfaceOp.startSeq,
401
+ end: surfaceOp.endSeq,
267
402
  ...range
268
403
  };
269
404
  }
@@ -371,9 +506,9 @@ var SurfaceManager = class {
371
506
  * @module dsh-session/request-header
372
507
  */
373
508
  /**
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.
509
+ * Normalize a header to canonical form: an empty tool list becomes an absent
510
+ * field, matching how requests are built. Logging, folding, and comparison use
511
+ * this one representation.
377
512
  * @param header - the header to normalize (not mutated).
378
513
  * @returns the canonical header.
379
514
  */
@@ -382,7 +517,6 @@ function canonicalHeader(header) {
382
517
  return {
383
518
  config: header.config,
384
519
  ...adapterDefaults?.reasoningEffort === true || adapterDefaults?.maxTokens === true ? { adapterDefaults } : {},
385
- ...header.system !== void 0 && header.system.length > 0 ? { system: header.system } : {},
386
520
  ...header.tools !== void 0 && header.tools.length > 0 ? { tools: header.tools } : {}
387
521
  };
388
522
  }
@@ -394,10 +528,10 @@ function sameSchema(a, b) {
394
528
  * Field-wise equality over canonical headers. Tool schemas compare in order.
395
529
  * @param a - one canonical header.
396
530
  * @param b - the other.
397
- * @returns whether config, system, and tools all match.
531
+ * @returns whether config, adapter defaults, and tools all match.
398
532
  */
399
533
  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;
534
+ if (!callConfigEquals(a.config, b.config) || a.adapterDefaults?.reasoningEffort !== b.adapterDefaults?.reasoningEffort || a.adapterDefaults?.maxTokens !== b.adapterDefaults?.maxTokens) return false;
401
535
  const at = a.tools ?? [];
402
536
  const bt = b.tools ?? [];
403
537
  return at.length === bt.length && at.every((tool, i) => sameSchema(tool, bt[i]));
@@ -575,83 +709,6 @@ function interruptedTurnClosers(events) {
575
709
  return closers;
576
710
  }
577
711
  //#endregion
578
- //#region lib/types/known-event-types.js
579
- /**
580
- * GENERATED by `scripts/gen-persistence-catalog.ts` — do not edit by hand; run
581
- * `pnpm run gen-persistence-catalog` to regenerate (verified fresh by
582
- * `pnpm run verify-persistence-catalog`, part of `doc-sync`).
583
- * @module @deepseek-ai/dsh-session/known-event-types
584
- */
585
- /**
586
- * Every `SessionEventMap` member declared in this repository — the event
587
- * vocabulary this build understands. The persistence read path refuses to
588
- * interpret a log containing a type outside this set unless the event
589
- * carries the envelope's `ignorable` marker (see `SessionEvent.ignorable`
590
- * in `./types.ts`): such a log was likely written by a newer harness, and
591
- * silently skipping a required event would reconstruct a wrong session.
592
- * Downstream (out-of-repo) plugin events are outside this list by
593
- * construction. The persisted `SessionEvent.ignorable` marker is the
594
- * compatibility mechanism; event-name registration was rejected because
595
- * it does not classify omission safety and would make reads
596
- * composition-dependent. The rationale is in
597
- * `.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.md`.
598
- */
599
- const KNOWN_SESSION_EVENT_TYPES = new Set([
600
- "agent-preset/selected",
601
- "agent/inbox/spliced",
602
- "approval/asked",
603
- "approval/decided",
604
- "approval/policy",
605
- "assistant/attempt",
606
- "assistant/message",
607
- "command/done",
608
- "command/run",
609
- "compaction/end",
610
- "compaction/prune",
611
- "compaction/start",
612
- "compaction/summary",
613
- "feedback/message-delete",
614
- "feedback/message-put",
615
- "feedback/record",
616
- "goal/change",
617
- "hook/invoked",
618
- "hook/result",
619
- "llm/retry",
620
- "llm/retry-started",
621
- "model/selection",
622
- "permission/preset",
623
- "plan/mode",
624
- "request/context",
625
- "request/header",
626
- "sandbox/mode",
627
- "schedule/change",
628
- "session-log-deepseek/delivery-accepted",
629
- "session/end-seed",
630
- "session/title",
631
- "session/title-llm-request",
632
- "step/end",
633
- "step/start",
634
- "subagent/descriptor",
635
- "subagent/model-selection-policy",
636
- "team/member",
637
- "team/message/delivered",
638
- "team/message/queued",
639
- "team/task",
640
- "todo/write",
641
- "tool-workflow/agent-end",
642
- "tool-workflow/agent-start",
643
- "tool-workflow/run-end",
644
- "tool-workflow/run-start",
645
- "tool/call",
646
- "tool/code-dispatch",
647
- "tool/code-dispatch-start",
648
- "tool/result",
649
- "turn/end",
650
- "turn/start",
651
- "user/message",
652
- "web/deepseek-search-llm-request"
653
- ]);
654
- //#endregion
655
712
  //#region lib/types/seq-ranges.js
656
713
  /** Lossless range encoding for JSONL `sourceEventSeqs` arrays. */
657
714
  function isStrictlyIncreasing(values) {
@@ -721,7 +778,7 @@ function validateSessionHeader(id, input) {
721
778
  if (input === null || typeof input !== "object" || Array.isArray(input)) throw new Error("session header is not a plain JSON record");
722
779
  const record = input;
723
780
  if (Object.hasOwn(record, "seedLength")) throw new Error("session header has invalid field \"seedLength\"");
724
- if (record.version !== 2) throw new Error(`session header version must be 2, got ${String(record.version)}`);
781
+ if (record.version !== 3) throw new Error(`session header version must be 3, got ${String(record.version)}`);
725
782
  if (record.id !== id) throw new Error(`session header id "${String(record.id)}" does not match session id "${id}"`);
726
783
  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");
727
784
  if (record.cwd !== void 0) {
@@ -746,7 +803,7 @@ function validateRestoredSessionHeader(id, input) {
746
803
  /** Detach, validate, and freeze the creation metadata published by a session. */
747
804
  function snapshotSessionHeader(id, source) {
748
805
  const snapshot = snapshotJsonValue(source === void 0 ? {
749
- version: 2,
806
+ version: 3,
750
807
  id,
751
808
  createdAt: Date.now(),
752
809
  isSeeded: false
@@ -761,13 +818,17 @@ function snapshotSessionHeader(id, source) {
761
818
  * Use {@link snapshotSessionEvent} when exclusive ownership is not guaranteed.
762
819
  * @param event - exclusively owned event imported across a trusted boundary.
763
820
  * @returns the same event object with a validated, deeply frozen message.
821
+ * @throws when event-local surface metadata, request-header fields, or message invariants are invalid; history relations are not checked.
764
822
  */
765
823
  function adoptSessionEvent(event) {
824
+ validateSessionEventData(event, `session event at seq ${event.seq}`);
825
+ validateSurfaceMetadata(event);
766
826
  assertMessageEventShape(event, `session event at seq ${event.seq}`);
767
827
  switch (event.type) {
768
828
  case "user/message":
769
829
  deepFreeze(event.data);
770
830
  break;
831
+ case "system/message":
771
832
  case "assistant/message":
772
833
  case "tool/result":
773
834
  deepFreeze(event.data.message);
@@ -786,6 +847,7 @@ function snapshotSessionEvent(event) {
786
847
  }
787
848
  /** Validate the fixed event envelope after one-pass JSON materialization. */
788
849
  function assertSessionEventEnvelope(value, index) {
850
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`seed event at index ${index} has an invalid event envelope`);
789
851
  const event = value;
790
852
  for (const key in event) switch (key) {
791
853
  case "type":
@@ -801,8 +863,10 @@ function assertSessionEventEnvelope(value, index) {
801
863
  const seq = event["seq"];
802
864
  const time = event["time"];
803
865
  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`);
866
+ validateSessionEventData(event, `seed ${type} at index ${index}`);
804
867
  switch (type) {
805
868
  case "request/header":
869
+ case "system/message":
806
870
  case "user/message":
807
871
  case "assistant/attempt":
808
872
  case "assistant/message":
@@ -816,14 +880,13 @@ function assertCurrentLlmShape(event, index) {
816
880
  const data = event["data"];
817
881
  const record = typeof data === "object" && data !== null ? data : void 0;
818
882
  if (event["type"] === "request/header") {
819
- const header = record?.["header"];
820
- const headerRecord = typeof header === "object" && header !== null && !Array.isArray(header) ? header : void 0;
821
- const config = headerRecord?.["config"];
883
+ const headerRecord = record?.["header"];
884
+ const config = headerRecord["config"];
822
885
  if (!hasProviderModel(config)) throw new Error(`seed request/header at index ${index} lacks provider/model`);
823
886
  const configRecord = config;
824
887
  const reasoningEffort = configRecord["reasoningEffort"];
825
888
  if (reasoningEffort !== void 0 && (typeof reasoningEffort !== "string" || reasoningEffort.length === 0)) throw new Error(`seed request/header at index ${index} has an invalid reasoningEffort`);
826
- assertAdapterDefaults(headerRecord?.["adapterDefaults"], configRecord, index);
889
+ assertAdapterDefaults(headerRecord["adapterDefaults"], configRecord, index);
827
890
  const reason = record?.["reason"];
828
891
  if (reason !== "initial" && reason !== "resume" && reason !== "change" && reason !== "series") throw new Error(`seed request/header at index ${index} has an invalid reason`);
829
892
  if (record?.["startsSeries"] !== void 0 && record["startsSeries"] !== true) throw new Error(`seed request/header at index ${index} has an invalid startsSeries marker`);
@@ -833,7 +896,7 @@ function assertCurrentLlmShape(event, index) {
833
896
  assertAssistantSettlementShape(record, type, index);
834
897
  return;
835
898
  }
836
- if (type !== "user/message" && type !== "assistant/message" && type !== "tool/result") return;
899
+ if (!isMessageEventType(type)) return;
837
900
  assertMessageEventShape(event, `seed ${type} at index ${index}`);
838
901
  if (type === "assistant/message") assertAssistantSettlementShape(record, type, index);
839
902
  }
@@ -851,21 +914,35 @@ function assertAdapterDefaults(value, config, index) {
851
914
  const defaults = value;
852
915
  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`);
853
916
  }
917
+ /** The four surface event types whose payload carries an identified message. */
918
+ function isMessageEventType(type) {
919
+ return type === "system/message" || type === "user/message" || type === "assistant/message" || type === "tool/result";
920
+ }
921
+ const MESSAGE_ROLE_BY_TYPE = {
922
+ "system/message": "system",
923
+ "user/message": "user",
924
+ "assistant/message": "assistant",
925
+ "tool/result": "user"
926
+ };
854
927
  /** Validate only the event-specific invariants needed to safely replay a message. */
855
928
  function assertMessageEventShape(event, subject) {
856
929
  const type = event["type"];
857
- if (type !== "user/message" && type !== "assistant/message" && type !== "tool/result") return;
930
+ if (!isMessageEventType(type)) return;
858
931
  const data = event["data"];
859
932
  const record = typeof data === "object" && data !== null ? data : void 0;
860
933
  const message = type === "user/message" ? record : record?.["message"];
861
934
  if (typeof message !== "object" || message === null || typeof message["id"] !== "string" || message["id"] === "") throw new Error(`${subject} lacks an identified message`);
862
935
  const messageRecord = message;
863
- const expectedRole = type === "assistant/message" ? "assistant" : "user";
936
+ const expectedRole = MESSAGE_ROLE_BY_TYPE[type];
864
937
  if (messageRecord["role"] !== expectedRole) throw new Error(`${subject} message must have role "${expectedRole}"`);
865
938
  const source = messageRecord["source"];
866
939
  if (typeof source !== "object" || source === null || typeof source["kind"] !== "string" || source["kind"] === "") throw new Error(`${subject} message has invalid source`);
867
940
  if (!Array.isArray(messageRecord["content"])) throw new Error(`${subject} message has invalid content`);
868
941
  const sourceRecord = source;
942
+ if (type === "system/message") {
943
+ if (sourceRecord["kind"] !== "plugin" || typeof sourceRecord["plugin"] !== "string" || sourceRecord["plugin"] === "") throw new Error(`${subject} message must have plugin source`);
944
+ return;
945
+ }
869
946
  if (type === "assistant/message") {
870
947
  if (sourceRecord["kind"] !== "model" || !hasProviderModel(sourceRecord)) throw new Error(`${subject} message must have model source`);
871
948
  return;
@@ -1079,6 +1156,7 @@ var Session = class Session {
1079
1156
  * (BigInt, function, symbol, undefined, negative zero, non-finite number,
1080
1157
  * circular reference, sparse array, or an exotic object such as
1081
1158
  * Map/Set/Date/class instance), or when the candidate violates the
1159
+ * request-header empty-field or tool-error consistency rules, or the
1082
1160
  * canonical surface contract (marker shape and eligibility, unique
1083
1161
  * earlier source-event references, positional replacement validity, and complete
1084
1162
  * shadowed-node coverage). One iterative pass reads, validates, and
@@ -1108,6 +1186,7 @@ var Session = class Session {
1108
1186
  data: dataSnapshot,
1109
1187
  ...surfaceMetadataSnapshot
1110
1188
  });
1189
+ validateSessionEventData(event, `session event "${type}" at seq ${event.seq}`);
1111
1190
  this.surfaceManager.validateNext(event);
1112
1191
  if (entry !== void 0) entry.appending = true;
1113
1192
  try {
@@ -1312,7 +1391,7 @@ var SessionStore = class extends Service {
1312
1391
  const seed = options?.seed;
1313
1392
  const meta = options?.meta;
1314
1393
  const header = {
1315
- version: 2,
1394
+ version: 3,
1316
1395
  id: sessionId,
1317
1396
  createdAt: meta?.createdAt ?? Date.now(),
1318
1397
  ...meta?.cwd === void 0 ? {} : { cwd: meta.cwd },
package/lib/invariant.js CHANGED
@@ -78,6 +78,9 @@ function validateEvent(trace, event, fail) {
78
78
  };
79
79
  break;
80
80
  }
81
+ case "system/message":
82
+ requireOpenStep(trace, "system/message", event.data.turn, event.data.step, fail);
83
+ break;
81
84
  case "user/message": break;
82
85
  case "session/end-seed": break;
83
86
  case "request/header":
@@ -15,7 +15,7 @@ import type { SessionSurface } from './surface.ts';
15
15
  export * from './types.ts';
16
16
  export { SessionPreparation } from './preparation.ts';
17
17
  export type { SessionPreparationOptions } from './preparation.ts';
18
- export type { AssistantMessage, ToolResultMessage, UserMessage } from '@deepseek-ai/dsh-llm';
18
+ export type { AssistantMessage, SystemMessage, ToolResultMessage, UserMessage } from '@deepseek-ai/dsh-llm';
19
19
  export { interruptedTurnClosers, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from './repair.ts';
20
20
  export type { SessionSurface, SurfaceFoldReplacement, SurfaceFoldResult } from './surface.ts';
21
21
  export { deriveEventMessage, foldSurface, isAppendSurfaceEvent, isReplacementSurfaceEvent, isSurfaceEvent, isSurfaceEligibleType } from './surface.ts';
@@ -83,6 +83,7 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
83
83
  * Use {@link snapshotSessionEvent} when exclusive ownership is not guaranteed.
84
84
  * @param event - exclusively owned event imported across a trusted boundary.
85
85
  * @returns the same event object with a validated, deeply frozen message.
86
+ * @throws when event-local surface metadata, request-header fields, or message invariants are invalid; history relations are not checked.
86
87
  */
87
88
  export declare function adoptSessionEvent<T extends SessionEvent>(event: T): T;
88
89
  /**
@@ -223,6 +224,7 @@ export declare class Session {
223
224
  * (BigInt, function, symbol, undefined, negative zero, non-finite number,
224
225
  * circular reference, sparse array, or an exotic object such as
225
226
  * Map/Set/Date/class instance), or when the candidate violates the
227
+ * request-header empty-field or tool-error consistency rules, or the
226
228
  * canonical surface contract (marker shape and eligibility, unique
227
229
  * earlier source-event references, positional replacement validity, and complete
228
230
  * shadowed-node coverage). One iterative pass reads, validates, and