@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/README.i18n.yaml +2 -2
- package/README.md +13 -9
- package/README.zh.md +13 -9
- package/lib/index.js +190 -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 +5 -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 +45 -40
- package/lib/types/types.js +1 -1
- package/package.json +9 -9
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 =
|
|
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
|
|
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, "
|
|
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.
|
|
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.
|
|
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 =
|
|
387
|
+
const surfaceOp = validateSurfaceMetadata(event);
|
|
251
388
|
if (surfaceOp === void 0) return;
|
|
252
|
-
if (surfaceOp === "append") {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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.
|
|
266
|
-
end: surfaceOp.
|
|
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
|
|
375
|
-
*
|
|
376
|
-
*
|
|
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,
|
|
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
|
|
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 !==
|
|
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:
|
|
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
|
|
820
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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":
|
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
|