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