@deepseek-ai/dsh-session 0.1.3-alpha.2 → 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
@@ -53,7 +53,85 @@ 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
+ "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
+ ]);
57
135
  //#endregion
58
136
  //#region lib/types/surface.js
59
137
  /**
@@ -67,6 +145,7 @@ const SESSION_FORMAT_VERSION = 2;
67
145
  */
68
146
  /** Runtime counterpart of the message-producing event union. */
69
147
  const SURFACE_EVENT_TYPES = new Set([
148
+ "system/message",
70
149
  "user/message",
71
150
  "assistant/message",
72
151
  "tool/result"
@@ -74,7 +153,7 @@ const SURFACE_EVENT_TYPES = new Set([
74
153
  /**
75
154
  * Whether an event type can join the model-visible surface.
76
155
  * @param type - event type to test.
77
- * @returns true for one of the three message-producing event types.
156
+ * @returns true for one of the four message-producing event types.
78
157
  */
79
158
  function isSurfaceEligibleType(type) {
80
159
  return SURFACE_EVENT_TYPES.has(type);
@@ -128,6 +207,7 @@ function isReplacementSurfaceEvent(event) {
128
207
  function deriveEventMessage(event) {
129
208
  switch (event.type) {
130
209
  case "user/message": return event.data;
210
+ case "system/message":
131
211
  case "assistant/message":
132
212
  if (event.data.message.content.length === 0) return null;
133
213
  return event.data.message;
@@ -135,6 +215,36 @@ function deriveEventMessage(event) {
135
215
  default: return null;
136
216
  }
137
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
+ }
138
248
  /** Create an empty surface fold state. */
139
249
  function createFoldState() {
140
250
  return {
@@ -149,12 +259,13 @@ function isEventSeq(value) {
149
259
  /** Whether a runtime value is the exact positional-replacement shape. */
150
260
  function isReplaceOp(value) {
151
261
  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"]);
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"]);
153
263
  }
154
264
  /** Validate event-local surface eligibility and return its operation. */
155
265
  function surfaceOpOf(event) {
156
266
  const raw = event;
157
267
  if (!isSurfaceEligibleType(event.type)) {
268
+ if (!KNOWN_SESSION_EVENT_TYPES.has(event.type) && event.ignorable === true) return;
158
269
  if (raw.surfaceOp !== void 0) throw new Error(`session event "${event.type}" is not surface-eligible and cannot carry surfaceOp`);
159
270
  if (raw.sourceEventSeqs !== void 0) throw new Error(`session event "${event.type}" is not surface-eligible and cannot carry sourceEventSeqs`);
160
271
  return;
@@ -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,83 +707,6 @@ function interruptedTurnClosers(events) {
575
707
  return closers;
576
708
  }
577
709
  //#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
710
  //#region lib/types/seq-ranges.js
656
711
  /** Lossless range encoding for JSONL `sourceEventSeqs` arrays. */
657
712
  function isStrictlyIncreasing(values) {
@@ -721,7 +776,7 @@ function validateSessionHeader(id, input) {
721
776
  if (input === null || typeof input !== "object" || Array.isArray(input)) throw new Error("session header is not a plain JSON record");
722
777
  const record = input;
723
778
  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)}`);
779
+ if (record.version !== 3) throw new Error(`session header version must be 3, got ${String(record.version)}`);
725
780
  if (record.id !== id) throw new Error(`session header id "${String(record.id)}" does not match session id "${id}"`);
726
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");
727
782
  if (record.cwd !== void 0) {
@@ -746,7 +801,7 @@ function validateRestoredSessionHeader(id, input) {
746
801
  /** Detach, validate, and freeze the creation metadata published by a session. */
747
802
  function snapshotSessionHeader(id, source) {
748
803
  const snapshot = snapshotJsonValue(source === void 0 ? {
749
- version: 2,
804
+ version: 3,
750
805
  id,
751
806
  createdAt: Date.now(),
752
807
  isSeeded: false
@@ -761,13 +816,17 @@ function snapshotSessionHeader(id, source) {
761
816
  * Use {@link snapshotSessionEvent} when exclusive ownership is not guaranteed.
762
817
  * @param event - exclusively owned event imported across a trusted boundary.
763
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.
764
820
  */
765
821
  function adoptSessionEvent(event) {
822
+ validateSessionEventData(event, `session event at seq ${event.seq}`);
823
+ validateSurfaceMetadata(event);
766
824
  assertMessageEventShape(event, `session event at seq ${event.seq}`);
767
825
  switch (event.type) {
768
826
  case "user/message":
769
827
  deepFreeze(event.data);
770
828
  break;
829
+ case "system/message":
771
830
  case "assistant/message":
772
831
  case "tool/result":
773
832
  deepFreeze(event.data.message);
@@ -786,6 +845,7 @@ function snapshotSessionEvent(event) {
786
845
  }
787
846
  /** Validate the fixed event envelope after one-pass JSON materialization. */
788
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`);
789
849
  const event = value;
790
850
  for (const key in event) switch (key) {
791
851
  case "type":
@@ -801,8 +861,10 @@ function assertSessionEventEnvelope(value, index) {
801
861
  const seq = event["seq"];
802
862
  const time = event["time"];
803
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}`);
804
865
  switch (type) {
805
866
  case "request/header":
867
+ case "system/message":
806
868
  case "user/message":
807
869
  case "assistant/attempt":
808
870
  case "assistant/message":
@@ -816,14 +878,13 @@ function assertCurrentLlmShape(event, index) {
816
878
  const data = event["data"];
817
879
  const record = typeof data === "object" && data !== null ? data : void 0;
818
880
  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"];
881
+ const headerRecord = record?.["header"];
882
+ const config = headerRecord["config"];
822
883
  if (!hasProviderModel(config)) throw new Error(`seed request/header at index ${index} lacks provider/model`);
823
884
  const configRecord = config;
824
885
  const reasoningEffort = configRecord["reasoningEffort"];
825
886
  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);
887
+ assertAdapterDefaults(headerRecord["adapterDefaults"], configRecord, index);
827
888
  const reason = record?.["reason"];
828
889
  if (reason !== "initial" && reason !== "resume" && reason !== "change" && reason !== "series") throw new Error(`seed request/header at index ${index} has an invalid reason`);
829
890
  if (record?.["startsSeries"] !== void 0 && record["startsSeries"] !== true) throw new Error(`seed request/header at index ${index} has an invalid startsSeries marker`);
@@ -833,7 +894,7 @@ function assertCurrentLlmShape(event, index) {
833
894
  assertAssistantSettlementShape(record, type, index);
834
895
  return;
835
896
  }
836
- if (type !== "user/message" && type !== "assistant/message" && type !== "tool/result") return;
897
+ if (!isMessageEventType(type)) return;
837
898
  assertMessageEventShape(event, `seed ${type} at index ${index}`);
838
899
  if (type === "assistant/message") assertAssistantSettlementShape(record, type, index);
839
900
  }
@@ -851,21 +912,35 @@ function assertAdapterDefaults(value, config, index) {
851
912
  const defaults = value;
852
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`);
853
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
+ };
854
925
  /** Validate only the event-specific invariants needed to safely replay a message. */
855
926
  function assertMessageEventShape(event, subject) {
856
927
  const type = event["type"];
857
- if (type !== "user/message" && type !== "assistant/message" && type !== "tool/result") return;
928
+ if (!isMessageEventType(type)) return;
858
929
  const data = event["data"];
859
930
  const record = typeof data === "object" && data !== null ? data : void 0;
860
931
  const message = type === "user/message" ? record : record?.["message"];
861
932
  if (typeof message !== "object" || message === null || typeof message["id"] !== "string" || message["id"] === "") throw new Error(`${subject} lacks an identified message`);
862
933
  const messageRecord = message;
863
- const expectedRole = type === "assistant/message" ? "assistant" : "user";
934
+ const expectedRole = MESSAGE_ROLE_BY_TYPE[type];
864
935
  if (messageRecord["role"] !== expectedRole) throw new Error(`${subject} message must have role "${expectedRole}"`);
865
936
  const source = messageRecord["source"];
866
937
  if (typeof source !== "object" || source === null || typeof source["kind"] !== "string" || source["kind"] === "") throw new Error(`${subject} message has invalid source`);
867
938
  if (!Array.isArray(messageRecord["content"])) throw new Error(`${subject} message has invalid content`);
868
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
+ }
869
944
  if (type === "assistant/message") {
870
945
  if (sourceRecord["kind"] !== "model" || !hasProviderModel(sourceRecord)) throw new Error(`${subject} message must have model source`);
871
946
  return;
@@ -1079,6 +1154,7 @@ var Session = class Session {
1079
1154
  * (BigInt, function, symbol, undefined, negative zero, non-finite number,
1080
1155
  * circular reference, sparse array, or an exotic object such as
1081
1156
  * Map/Set/Date/class instance), or when the candidate violates the
1157
+ * request-header empty-field or tool-error consistency rules, or the
1082
1158
  * canonical surface contract (marker shape and eligibility, unique
1083
1159
  * earlier source-event references, positional replacement validity, and complete
1084
1160
  * shadowed-node coverage). One iterative pass reads, validates, and
@@ -1108,6 +1184,7 @@ var Session = class Session {
1108
1184
  data: dataSnapshot,
1109
1185
  ...surfaceMetadataSnapshot
1110
1186
  });
1187
+ validateSessionEventData(event, `session event "${type}" at seq ${event.seq}`);
1111
1188
  this.surfaceManager.validateNext(event);
1112
1189
  if (entry !== void 0) entry.appending = true;
1113
1190
  try {
@@ -1312,7 +1389,7 @@ var SessionStore = class extends Service {
1312
1389
  const seed = options?.seed;
1313
1390
  const meta = options?.meta;
1314
1391
  const header = {
1315
- version: 2,
1392
+ version: 3,
1316
1393
  id: sessionId,
1317
1394
  createdAt: meta?.createdAt ?? Date.now(),
1318
1395
  ...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