@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/README.i18n.yaml +2 -2
- package/README.md +13 -9
- package/README.zh.md +13 -9
- package/lib/index.js +188 -111
- package/lib/invariant.js +3 -0
- package/lib/types/index.d.ts +3 -1
- package/lib/types/index.js +36 -12
- package/lib/types/invariant.js +4 -0
- package/lib/types/known-event-types.js +3 -2
- package/lib/types/request-header.d.ts +4 -4
- package/lib/types/request-header.js +5 -7
- package/lib/types/surface.d.ts +17 -1
- package/lib/types/surface.js +102 -18
- package/lib/types/types.d.ts +44 -39
- package/lib/types/types.js +1 -1
- package/package.json +10 -10
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 =
|
|
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
|
|
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, "
|
|
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.
|
|
192
|
-
if (startIdx === -1) throw new Error(`surface replace: start seq ${op.
|
|
193
|
-
const endIdx = state.nodes.indexOf(op.
|
|
194
|
-
if (endIdx === -1) throw new Error(`surface replace: end seq ${op.
|
|
195
|
-
if (startIdx > endIdx) throw new Error(`surface replace: start seq ${op.
|
|
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 =
|
|
385
|
+
const surfaceOp = validateSurfaceMetadata(event);
|
|
251
386
|
if (surfaceOp === void 0) return;
|
|
252
|
-
if (surfaceOp === "append") {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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.
|
|
266
|
-
end: surfaceOp.
|
|
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
|
|
375
|
-
*
|
|
376
|
-
*
|
|
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,
|
|
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
|
|
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 !==
|
|
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:
|
|
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
|
|
820
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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":
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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
|