@deepseek-ai/dsh-session 0.0.1-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js ADDED
@@ -0,0 +1,1841 @@
1
+ import { Service } from "@deepseek-ai/cordis";
2
+ import { isAbsolute } from "node:path";
3
+ import { CallId, MessageId, assertNever, callConfigEquals, deepFreeze, freezeMessage } from "@deepseek-ai/dsh-llm";
4
+ import { scopeOf, scopeTarget } from "@deepseek-ai/dsh-scope";
5
+ //#region lib/types/types.js
6
+ /**
7
+ * Brand a string as a {@link SessionId}.
8
+ * @param id - the raw session id string.
9
+ * @returns the same string, branded (a compile-time cast — no runtime cost).
10
+ */
11
+ function SessionId(id) {
12
+ return id;
13
+ }
14
+ /**
15
+ * The on-disk session format version, stamped into every newly-written {@link SessionHeader}
16
+ * and enforced by every persistence backend on load. The single source of truth for the
17
+ * version — write sites and the load-time check all read it.
18
+ * While the harness is unreleased it is pinned at `0`: no compatibility is
19
+ * implied, incompatible logs are rejected, and no migration is provided. A
20
+ * monotonic version policy starts with the first tagged release.
21
+ */
22
+ const SESSION_FORMAT_VERSION = 0;
23
+ //#endregion
24
+ //#region lib/types/json.js
25
+ /** Lossless-JSON validation and detached snapshots for durable session data. @module @deepseek-ai/dsh-session/json */
26
+ /** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
27
+ function hasIntrinsicConstructor(prototype, name) {
28
+ const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor")?.value;
29
+ if (typeof constructor !== "function") return false;
30
+ try {
31
+ return constructor.name === name && constructor.prototype === prototype && Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+ /** Whether a candidate is one realm's intrinsic `Object.prototype`. */
37
+ function isIntrinsicObjectPrototype(value) {
38
+ return Object.getPrototypeOf(value) === null && hasIntrinsicConstructor(value, "Object");
39
+ }
40
+ /** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */
41
+ function hasPlainArrayPrototype(value) {
42
+ const prototype = Object.getPrototypeOf(value);
43
+ if (!Array.isArray(prototype) || !hasIntrinsicConstructor(prototype, "Array")) return false;
44
+ const objectPrototype = Object.getPrototypeOf(prototype);
45
+ return typeof objectPrototype === "object" && objectPrototype !== null && isIntrinsicObjectPrototype(objectPrototype);
46
+ }
47
+ /** Whether an object is a plain or null-prototype record from any JavaScript realm. */
48
+ function hasPlainObjectPrototype(value) {
49
+ const prototype = Object.getPrototypeOf(value);
50
+ return prototype === null || typeof prototype === "object" && isIntrinsicObjectPrototype(prototype);
51
+ }
52
+ /** Return every JSON-visible object key, or reject own data JSON would discard. */
53
+ function enumerableStringKeys(value) {
54
+ const keys = Reflect.ownKeys(value);
55
+ if (keys.some((key) => typeof key !== "string" || !Object.prototype.propertyIsEnumerable.call(value, key))) return void 0;
56
+ return keys;
57
+ }
58
+ /** Validate lossless JSON iteratively, optionally materializing a detached snapshot. */
59
+ function walkJsonValue(value, detach) {
60
+ const ancestors = /* @__PURE__ */ new Set();
61
+ let root;
62
+ const assign = (destination, item) => {
63
+ if (destination === void 0) return;
64
+ if (destination.kind === "root") root = item;
65
+ else if (destination.kind === "array") destination.target[destination.index] = item;
66
+ else Object.defineProperty(destination.target, destination.key, {
67
+ value: item,
68
+ enumerable: true,
69
+ configurable: true,
70
+ writable: true
71
+ });
72
+ };
73
+ const tasks = [{
74
+ kind: "visit",
75
+ value,
76
+ ...detach ? { destination: { kind: "root" } } : {}
77
+ }];
78
+ for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
79
+ if (task.kind === "leave") {
80
+ ancestors.delete(task.source);
81
+ continue;
82
+ }
83
+ if (task.kind === "array-item") {
84
+ if (!Object.prototype.hasOwnProperty.call(task.source, task.index)) return void 0;
85
+ tasks.push({
86
+ kind: "visit",
87
+ value: task.source[task.index],
88
+ ...task.target === void 0 ? {} : { destination: {
89
+ kind: "array",
90
+ target: task.target,
91
+ index: task.index
92
+ } }
93
+ });
94
+ continue;
95
+ }
96
+ if (task.kind === "object-property") {
97
+ tasks.push({
98
+ kind: "visit",
99
+ value: task.source[task.key],
100
+ ...task.target === void 0 ? {} : { destination: {
101
+ kind: "object",
102
+ target: task.target,
103
+ key: task.key
104
+ } }
105
+ });
106
+ continue;
107
+ }
108
+ const current = task.value;
109
+ if (current === null) {
110
+ assign(task.destination, null);
111
+ continue;
112
+ }
113
+ if (typeof current === "boolean" || typeof current === "string") {
114
+ assign(task.destination, current);
115
+ continue;
116
+ }
117
+ if (typeof current === "number") {
118
+ if (!Number.isFinite(current) || Object.is(current, -0)) return void 0;
119
+ assign(task.destination, current);
120
+ continue;
121
+ }
122
+ if (typeof current !== "object") return void 0;
123
+ if (ancestors.has(current)) return void 0;
124
+ if (Array.isArray(current)) {
125
+ if (!hasPlainArrayPrototype(current)) return void 0;
126
+ const length = current.length;
127
+ if (Reflect.ownKeys(current).length !== length + 1) return void 0;
128
+ const target = detach ? [] : void 0;
129
+ if (target !== void 0) assign(task.destination, target);
130
+ ancestors.add(current);
131
+ tasks.push({
132
+ kind: "leave",
133
+ source: current
134
+ });
135
+ for (let index = length - 1; index >= 0; index--) tasks.push({
136
+ kind: "array-item",
137
+ source: current,
138
+ index,
139
+ ...target === void 0 ? {} : { target }
140
+ });
141
+ continue;
142
+ }
143
+ if (!hasPlainObjectPrototype(current)) return void 0;
144
+ const keys = enumerableStringKeys(current);
145
+ if (keys === void 0) return void 0;
146
+ const target = detach ? {} : void 0;
147
+ if (target !== void 0) assign(task.destination, target);
148
+ ancestors.add(current);
149
+ tasks.push({
150
+ kind: "leave",
151
+ source: current
152
+ });
153
+ for (let index = keys.length - 1; index >= 0; index--) {
154
+ const key = keys[index];
155
+ /* v8 ignore next -- the loop is bounded by the captured key count. */
156
+ if (key === void 0) return void 0;
157
+ tasks.push({
158
+ kind: "object-property",
159
+ source: current,
160
+ key,
161
+ ...target === void 0 ? {} : { target }
162
+ });
163
+ }
164
+ }
165
+ return detach ? root : true;
166
+ }
167
+ /**
168
+ * Validate and detach lossless JSON in one read per property, so a stateful
169
+ * getter cannot change between validation and copying. Traversal is iterative,
170
+ * so valid nesting is bounded by available memory rather than the JavaScript
171
+ * call stack. Accepts ordinary arrays, plain or null-prototype objects, and JSON
172
+ * scalars; rejects sparse, cyclic, exotic, negative-zero, and non-finite values.
173
+ * Getter throws propagate.
174
+ *
175
+ * @param value - the candidate value to validate and detach.
176
+ * @returns the detached snapshot, or `undefined` when the value is not
177
+ * losslessly JSON-serializable.
178
+ */
179
+ function snapshotJsonValue(value) {
180
+ return walkJsonValue(value, true);
181
+ }
182
+ /**
183
+ * Test the same lossless JSON boundary as {@link snapshotJsonValue} without
184
+ * detaching it. Only own enumerable string properties participate; `toJSON`
185
+ * is ignored and getters run, so persistence boundaries use the snapshotter.
186
+ * @param value - the candidate event data to test.
187
+ * @returns whether `value` survives JSON round-trip losslessly.
188
+ */
189
+ function isJsonValue(value) {
190
+ return walkJsonValue(value, false) === true;
191
+ }
192
+ //#endregion
193
+ //#region lib/types/surface.js
194
+ /**
195
+ * Surface layer on top of the session event log: an ordered view of events
196
+ * that produce LLM messages. The append-only log remains the source of truth.
197
+ *
198
+ * Browser-safe: web clients consume this subpath export, so it must stay free
199
+ * of `node:` imports (they break the vite bundle).
200
+ *
201
+ * @module @deepseek-ai/dsh-session/surface
202
+ */
203
+ /** Runtime counterpart of the message-producing event union. */
204
+ const SURFACE_EVENT_TYPES = new Set([
205
+ "user/message",
206
+ "assistant/message",
207
+ "tool/result"
208
+ ]);
209
+ /**
210
+ * Whether an event type can join the model-visible surface.
211
+ * @param type - event type to test.
212
+ * @returns true for one of the three message-producing event types.
213
+ */
214
+ function isSurfaceEligibleType(type) {
215
+ return SURFACE_EVENT_TYPES.has(type);
216
+ }
217
+ /**
218
+ * Narrow an event to a surface-eligible event carrying its required marker.
219
+ * @param event - event to test.
220
+ * @returns true when both the type and marker identify a surface event.
221
+ */
222
+ function isSurfaceEvent(event) {
223
+ if (!SURFACE_EVENT_TYPES.has(event.type)) return false;
224
+ return event.surfaceOp !== void 0;
225
+ }
226
+ /**
227
+ * Narrow an event to an append-origin surface event: one that entered the
228
+ * surface at its own log position and was never itself a replacement copy.
229
+ *
230
+ * The model-visible surface deliberately shadows replaced ranges, so it is the
231
+ * wrong source for a human transcript — a landed replacement would erase
232
+ * conversation the user already saw. Append-origin events are that transcript's
233
+ * durable source material; replacement copies stay model-only.
234
+ * @param event - event to test.
235
+ * @returns true when the event appended to the surface tail.
236
+ */
237
+ function isAppendSurfaceEvent(event) {
238
+ return isSurfaceEvent(event) && event.surfaceOp === "append";
239
+ }
240
+ /**
241
+ * Narrow an event to a surface replacement: a node that shadowed an existing
242
+ * surface range instead of appending to the tail. The counterpart of
243
+ * {@link isAppendSurfaceEvent} over the two {@link SurfaceOp} variants.
244
+ * @param event - event to test.
245
+ * @returns true when the event replaced a surface range.
246
+ */
247
+ function isReplacementSurfaceEvent(event) {
248
+ return isSurfaceEvent(event) && event.surfaceOp !== "append";
249
+ }
250
+ /**
251
+ * Project a single event into the LLM message it derives to, or null when it
252
+ * produces none — a non-surface event (chunk, boundary, log-only record) or an
253
+ * empty-content assistant/message (which exists only to host usage). This is
254
+ * THE per-node projection rule: `Session.deriveMessages` folds it over the
255
+ * live surface, external reconstructors and pure projections fold the same
256
+ * function over a log prefix's surface to rebuild the exact messages any
257
+ * request was built from. The returned message is the already frozen message
258
+ * nested in the event wrapper and shared by delivery, durable history, and
259
+ * model requests.
260
+ * @param event - the event to project.
261
+ * @returns the derived message, or null when the event produces none.
262
+ */
263
+ function deriveEventMessage(event) {
264
+ switch (event.type) {
265
+ case "user/message": return event.data;
266
+ case "assistant/message":
267
+ if (event.data.message.content.length === 0) return null;
268
+ return event.data.message;
269
+ case "tool/result": return event.data.message;
270
+ default: return null;
271
+ }
272
+ }
273
+ /** Create an empty surface fold state. */
274
+ function createFoldState() {
275
+ return {
276
+ nodes: [],
277
+ replaceGeneration: 0
278
+ };
279
+ }
280
+ /** Whether a runtime value is a non-negative safe event sequence. */
281
+ function isEventSeq(value) {
282
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
283
+ }
284
+ /** Whether a runtime value is the exact positional-replacement shape. */
285
+ function isReplaceOp(value) {
286
+ const op = value;
287
+ return Object.keys(op).length === 3 && Object.hasOwn(op, "op") && Object.hasOwn(op, "start") && Object.hasOwn(op, "end") && op["op"] === "replace" && isEventSeq(op["start"]) && isEventSeq(op["end"]);
288
+ }
289
+ /** Validate event-local surface eligibility and return its operation. */
290
+ function surfaceOpOf(event) {
291
+ const raw = event;
292
+ if (!isSurfaceEligibleType(event.type)) {
293
+ if (raw.surfaceOp !== void 0) throw new Error(`session event "${event.type}" is not surface-eligible and cannot carry surfaceOp`);
294
+ if (raw.sourceEventSeqs !== void 0) throw new Error(`session event "${event.type}" is not surface-eligible and cannot carry sourceEventSeqs`);
295
+ return;
296
+ }
297
+ const op = raw.surfaceOp;
298
+ if (op === void 0) throw new Error(`session event "${event.type}" is surface-eligible and requires a surfaceOp marker`);
299
+ if (op === "append") return op;
300
+ if (op === null || typeof op !== "object" || Array.isArray(op)) throw new Error(`session event "${event.type}" carries an invalid surfaceOp`);
301
+ if (!isReplaceOp(op)) throw new Error(`session event "${event.type}" carries an invalid replace surfaceOp`);
302
+ return op;
303
+ }
304
+ /** Validate cited source-event seqs against prior log entries and the replacement range. */
305
+ function assertProvenance(event, shadowedSeqs) {
306
+ const raw = event.sourceEventSeqs;
307
+ const sources = /* @__PURE__ */ new Set();
308
+ if (raw !== void 0) {
309
+ if (!Array.isArray(raw)) throw new Error(`sourceEventSeqs on event at seq ${event.seq} must be an array when present`);
310
+ if (raw.length === 0 && event.type !== "assistant/message") throw new Error("sourceEventSeqs must not be empty except on assistant/message");
311
+ let nonEarlierSource;
312
+ for (const source of raw) {
313
+ if (!isEventSeq(source)) throw new Error(`session event "${event.type}" sourceEventSeqs must densely contain non-negative safe integers`);
314
+ sources.add(source);
315
+ if (nonEarlierSource === void 0 && source >= event.seq) nonEarlierSource = source;
316
+ }
317
+ if (sources.size !== raw.length) throw new Error("sourceEventSeqs must not contain duplicates");
318
+ if (nonEarlierSource !== void 0) throw new Error(`sourceEventSeqs must reference earlier events: ${nonEarlierSource} >= current seq ${event.seq}`);
319
+ }
320
+ const missing = shadowedSeqs.filter((seq) => !sources.has(seq));
321
+ if (missing.length > 0) throw new Error(`surface replace: sourceEventSeqs must include every shadowed surface node; missing ${missing.join(", ")}`);
322
+ }
323
+ /** Locate one replacement range without mutating the current fold state. */
324
+ function replacementRange(state, op) {
325
+ const startIdx = state.nodes.indexOf(op.start);
326
+ if (startIdx === -1) throw new Error(`surface replace: start seq ${op.start} not found in surface`);
327
+ const endIdx = state.nodes.indexOf(op.end);
328
+ if (endIdx === -1) throw new Error(`surface replace: end seq ${op.end} not found in surface`);
329
+ if (startIdx > endIdx) throw new Error(`surface replace: start seq ${op.start} (index ${startIdx}) is after end seq ${op.end} (index ${endIdx})`);
330
+ return {
331
+ startIdx,
332
+ endIdx,
333
+ shadowedSeqs: state.nodes.slice(startIdx, endIdx + 1)
334
+ };
335
+ }
336
+ /**
337
+ * Deep structural equality over the session-event JSON value domain
338
+ * (null/boolean/number/string, arrays, plain objects). Replaces
339
+ * `node:util`'s isDeepStrictEqual to keep this module browser-safe.
340
+ */
341
+ function isDeepEqualJson(a, b) {
342
+ if (a === b) return true;
343
+ if (Array.isArray(a) || Array.isArray(b)) {
344
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
345
+ return a.every((item, i) => isDeepEqualJson(item, b[i]));
346
+ }
347
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
348
+ const aKeys = Object.keys(a);
349
+ const bRecord = b;
350
+ if (aKeys.length !== Object.keys(b).length) return false;
351
+ return aKeys.every((key) => Object.hasOwn(b, key) && isDeepEqualJson(a[key], bRecord[key]));
352
+ }
353
+ /** Restrict a tool-result replacement to one current result's content. */
354
+ function assertToolResultRewrite(event, shadowedSeqs, events, baseSeq) {
355
+ if (event.type !== "tool/result") return;
356
+ if (shadowedSeqs.length !== 1) throw new Error("tool/result surface replacement must rewrite exactly one current node");
357
+ for (const originalSeq of shadowedSeqs) {
358
+ const original = events[originalSeq - baseSeq];
359
+ if (original?.type !== "tool/result") throw new Error("tool/result surface replacement must target a current tool/result");
360
+ const originalRest = { ...original.data };
361
+ const replacementRest = { ...event.data };
362
+ const originalResult = original.data.message.content[0];
363
+ const replacementResult = event.data.message.content[0];
364
+ originalRest["message"] = {
365
+ ...original.data.message,
366
+ content: [{
367
+ ...originalResult,
368
+ content: null
369
+ }]
370
+ };
371
+ replacementRest["message"] = {
372
+ ...event.data.message,
373
+ content: [{
374
+ ...replacementResult,
375
+ content: null
376
+ }]
377
+ };
378
+ if (!isDeepEqualJson(originalRest, replacementRest)) throw new Error("tool/result surface replacement may change only content");
379
+ }
380
+ }
381
+ /** Validate one event at its replay boundary and prepare its atomic fold transition. */
382
+ function planSurfaceEvent(state, event, expectedSeq, events, baseSeq) {
383
+ if (event.seq !== expectedSeq) throw new Error(`session event seq ${event.seq} is not contiguous; expected ${expectedSeq}`);
384
+ const surfaceOp = surfaceOpOf(event);
385
+ if (surfaceOp === void 0) return;
386
+ if (surfaceOp === "append") {
387
+ assertProvenance(event, []);
388
+ return {
389
+ kind: "append",
390
+ seq: event.seq
391
+ };
392
+ }
393
+ const range = replacementRange(state, surfaceOp);
394
+ assertProvenance(event, range.shadowedSeqs);
395
+ assertToolResultRewrite(event, range.shadowedSeqs, events, baseSeq);
396
+ return {
397
+ kind: "replace",
398
+ seq: event.seq,
399
+ start: surfaceOp.start,
400
+ end: surfaceOp.end,
401
+ ...range
402
+ };
403
+ }
404
+ /** Apply one event and return replacement metadata only when one occurred. */
405
+ function applySurfaceEvent(state, event, expectedSeq, events, baseSeq) {
406
+ return applySurfacePlan(state, planSurfaceEvent(state, event, expectedSeq, events, baseSeq));
407
+ }
408
+ /** Commit one previously validated surface transition. */
409
+ function applySurfacePlan(state, plan) {
410
+ if (plan?.kind === "append") state.nodes.push(plan.seq);
411
+ else if (plan?.kind === "replace") {
412
+ state.nodes.splice(plan.startIdx, plan.endIdx - plan.startIdx + 1, plan.seq);
413
+ state.replaceGeneration += 1;
414
+ }
415
+ if (plan?.kind !== "replace") return;
416
+ return {
417
+ seq: plan.seq,
418
+ start: plan.start,
419
+ end: plan.end,
420
+ shadowedSeqs: plan.shadowedSeqs
421
+ };
422
+ }
423
+ /**
424
+ * Replay a complete session log through the canonical surface fold.
425
+ * @param events - session events in contiguous seq order.
426
+ * @returns detached current sequences and replacement history.
427
+ * @throws when an event violates surface metadata, source-event references, range, or tool-result rewrite rules.
428
+ */
429
+ function foldSurface(events) {
430
+ const state = createFoldState();
431
+ const replacements = [];
432
+ for (const [index, event] of events.entries()) {
433
+ const replacement = applySurfaceEvent(state, event, index, events, 0);
434
+ if (replacement !== void 0) replacements.push(replacement);
435
+ }
436
+ return {
437
+ nodes: [...state.nodes],
438
+ replacements
439
+ };
440
+ }
441
+ /** Incremental ordered surface view and append-boundary validator. */
442
+ var SurfaceManager = class {
443
+ log;
444
+ baseSeq;
445
+ /** Shared transition state; replacement history is not retained. */
446
+ _state = createFoldState();
447
+ /** Last processed absolute seq. */
448
+ _lastProcessedSeq;
449
+ /** Candidate already validated by `validateNext`, pending exact log admission. */
450
+ _pendingPlan;
451
+ /**
452
+ * @param log - Contiguous complete log or loaded event window.
453
+ * @param baseSeq - Absolute sequence of the window's first event.
454
+ */
455
+ constructor(log, baseSeq = 0) {
456
+ this.log = log;
457
+ this.baseSeq = baseSeq;
458
+ this._lastProcessedSeq = baseSeq - 1;
459
+ }
460
+ /**
461
+ * Validate the next candidate without mutating the committed surface.
462
+ * @param event - candidate event that has not entered the log yet.
463
+ */
464
+ validateNext(event) {
465
+ if (this._lastProcessedSeq < this.baseSeq + this.log.length - 1) this._processDelta();
466
+ const expectedSeq = this.baseSeq + this.log.length;
467
+ this._pendingPlan = {
468
+ event,
469
+ expectedSeq,
470
+ plan: planSurfaceEvent(this._state, event, expectedSeq, this.log, this.baseSeq)
471
+ };
472
+ }
473
+ /** Monotonic count of folded positional replacements. */
474
+ get replaceGeneration() {
475
+ if (this._lastProcessedSeq < this.baseSeq + this.log.length - 1) this._processDelta();
476
+ return this._state.replaceGeneration;
477
+ }
478
+ /** Surface event sequences in model-visible order. */
479
+ get nodes() {
480
+ if (this._lastProcessedSeq < this.baseSeq + this.log.length - 1) this._processDelta();
481
+ return this._state.nodes;
482
+ }
483
+ /** Fold events appended since the previous access. */
484
+ _processDelta() {
485
+ const tailSeq = this.baseSeq + this.log.length - 1;
486
+ for (let seq = this._lastProcessedSeq + 1; seq <= tailSeq; seq++) {
487
+ const index = seq - this.baseSeq;
488
+ const event = this.log[index];
489
+ const pending = this._pendingPlan;
490
+ if (pending?.event === event && pending.expectedSeq === seq) applySurfacePlan(this._state, pending.plan);
491
+ else applySurfaceEvent(this._state, event, seq, this.log, this.baseSeq);
492
+ if (pending !== void 0 && pending.expectedSeq <= seq) this._pendingPlan = void 0;
493
+ this._lastProcessedSeq = seq;
494
+ }
495
+ }
496
+ };
497
+ //#endregion
498
+ //#region lib/types/request-header.js
499
+ /**
500
+ * Request-header reconstruction utilities over full `request/header` session
501
+ * events. Anyone holding a session log reconstructs the {@link EpochHeader}
502
+ * any request was built under by taking the latest canonical snapshot; the
503
+ * loop uses the same equality helper to avoid logging unchanged headers.
504
+ *
505
+ * @module dsh-session/request-header
506
+ */
507
+ /**
508
+ * Normalize a header to canonical form: an empty system prompt and empty tool
509
+ * list become absent fields, matching how requests are built. Logging, folding,
510
+ * and comparison use this one representation.
511
+ * @param header - the header to normalize (not mutated).
512
+ * @returns the canonical header.
513
+ */
514
+ function canonicalHeader(header) {
515
+ const adapterDefaults = header.adapterDefaults;
516
+ return {
517
+ config: header.config,
518
+ ...adapterDefaults?.reasoningEffort === true || adapterDefaults?.maxTokens === true ? { adapterDefaults } : {},
519
+ ...header.system !== void 0 && header.system.length > 0 ? { system: header.system } : {},
520
+ ...header.tools !== void 0 && header.tools.length > 0 ? { tools: header.tools } : {}
521
+ };
522
+ }
523
+ /** Canonical JSON equality for tool schemas assembled through the same path. */
524
+ function sameSchema(a, b) {
525
+ return JSON.stringify(a) === JSON.stringify(b);
526
+ }
527
+ /**
528
+ * Field-wise equality over canonical headers. Tool schemas compare in order.
529
+ * @param a - one canonical header.
530
+ * @param b - the other.
531
+ * @returns whether config, system, and tools all match.
532
+ */
533
+ function headerEquals(a, b) {
534
+ if (!callConfigEquals(a.config, b.config) || a.adapterDefaults?.reasoningEffort !== b.adapterDefaults?.reasoningEffort || a.adapterDefaults?.maxTokens !== b.adapterDefaults?.maxTokens || a.system !== b.system) return false;
535
+ const at = a.tools ?? [];
536
+ const bt = b.tools ?? [];
537
+ return at.length === bt.length && at.every((tool, i) => sameSchema(tool, bt[i]));
538
+ }
539
+ /**
540
+ * Fold the header events of a log (or any prefix) into the
541
+ * {@link EpochHeader} in force after the last snapshot. Non-header events are
542
+ * skipped. This is the pure offline reconstruction path; the live session
543
+ * tracks the same fold incrementally.
544
+ * @param events - session events in log order.
545
+ * @param from - a previously folded state to continue from.
546
+ * @returns the latest canonical header, or undefined when none exists yet.
547
+ */
548
+ function foldRequestHeader(events, from) {
549
+ let state = from;
550
+ for (const event of events) if (event.type === "request/header") state = canonicalHeader(event.data.header);
551
+ return state;
552
+ }
553
+ //#endregion
554
+ //#region lib/types/preparation.js
555
+ /**
556
+ * Ownership of one unpublished Session before registry publication.
557
+ * @module @deepseek-ai/dsh-session/preparation
558
+ */
559
+ /**
560
+ * One exact unpublished Session and the provider state that keeps it usable.
561
+ * Disposal is synchronous and idempotent. Providers decide whether release
562
+ * returns the Session to a cache or discards it; publication may consume that
563
+ * state before disposal, making the callback a no-op.
564
+ */
565
+ var SessionPreparation = class SessionPreparation {
566
+ options;
567
+ released = false;
568
+ /** The exact Session to use for setup and publication. */
569
+ session;
570
+ constructor(session, options) {
571
+ this.options = options;
572
+ this.session = session;
573
+ }
574
+ /**
575
+ * Wrap an unpublished Session in one preparation lifetime.
576
+ * @param session - exact unpublished Session.
577
+ * @param options - optional provider release behavior.
578
+ * @returns a preparation disposed after publication or rollback.
579
+ */
580
+ static create(session, options) {
581
+ return new SessionPreparation(session, options ?? {});
582
+ }
583
+ /** Release provider state once when this preparation leaves its caller. */
584
+ [Symbol.dispose]() {
585
+ if (this.released) return;
586
+ this.released = true;
587
+ this.options.release?.();
588
+ }
589
+ };
590
+ //#endregion
591
+ //#region lib/types/repair.js
592
+ /**
593
+ * Crash-recovery repair for an interrupted session log. It preserves a fully
594
+ * written final turn and supplies the missing tool, step, and turn boundaries
595
+ * needed to resume with a provider-valid transcript, plus the activity-time
596
+ * read that must skip the end-seed boundary — which this module does
597
+ * not write (`Session`'s constructor does) but whose synthetic closers can
598
+ * inherit that boundary's timestamp, the one real coupling between the two.
599
+ * @module @deepseek-ai/dsh-session/repair
600
+ */
601
+ /**
602
+ * The `time` of the log's last event representing actual work, skipping the
603
+ * `session/end-seed` boundary — picking a session up is not activity, so
604
+ * activity ordering must exclude it.
605
+ *
606
+ * Excluded by type, so a pickup time still leaks when a boundary is the last
607
+ * event of an open turn: {@link interruptedTurnClosers} copies it onto the
608
+ * synthetic `turn/end`, which this counts as work. Reachable only by seeding an
609
+ * unbalanced log directly — `load()` balances first.
610
+ * @param events - the log to scan, in seq order.
611
+ * @returns the latest non-boundary event's `time`, or undefined when there is none.
612
+ */
613
+ function lastActivityTime(events) {
614
+ return events.findLast((event) => event.type !== "session/end-seed")?.time;
615
+ }
616
+ /** Recovery code for an assistant tool request that never reached a recorded call start. */
617
+ const TOOL_NOT_STARTED = "TOOL_NOT_STARTED";
618
+ /** Recovery code for a recorded tool call whose completed outcome was not durably recorded. */
619
+ const TOOL_OUTCOME_UNKNOWN = "TOOL_OUTCOME_UNKNOWN";
620
+ /**
621
+ * Return deterministic synthetic events that close an open tail turn. Unmatched
622
+ * calls receive error results first, followed by an open `step/end` and an
623
+ * interrupted `turn/end`; sequences continue the log and timestamps reuse the
624
+ * last real event. A balanced or empty log returns no events.
625
+ *
626
+ * @param events - the loaded durable log to scan (a valid committed prefix, possibly with a crash tail).
627
+ * @returns the synthetic closer events to append after `events`, in order; empty when the log is already balanced.
628
+ */
629
+ function interruptedTurnClosers(events) {
630
+ let openTurn = null;
631
+ let openStep = null;
632
+ const pendingCalls = /* @__PURE__ */ new Map();
633
+ for (const event of events) switch (event.type) {
634
+ case "turn/start":
635
+ openTurn = event.data.turn;
636
+ openStep = null;
637
+ pendingCalls.clear();
638
+ break;
639
+ case "turn/end":
640
+ openTurn = null;
641
+ openStep = null;
642
+ pendingCalls.clear();
643
+ break;
644
+ case "step/start":
645
+ openStep = event.data.step;
646
+ break;
647
+ case "step/end":
648
+ pendingCalls.clear();
649
+ openStep = null;
650
+ break;
651
+ case "assistant/message":
652
+ for (const block of event.data.message.content) if (block.type === "tool-call") pendingCalls.set(block.id, { step: event.data.step });
653
+ break;
654
+ case "tool/call":
655
+ {
656
+ const entry = pendingCalls.get(event.data.callId);
657
+ if (entry) entry.callSeq = event.seq;
658
+ }
659
+ break;
660
+ case "tool/result":
661
+ pendingCalls.delete(event.data.message.source.callId);
662
+ break;
663
+ default: break;
664
+ }
665
+ const last = events.at(-1);
666
+ if (openTurn === null || last === void 0) return [];
667
+ let seq = last.seq + 1;
668
+ const time = last.time;
669
+ const closers = [];
670
+ for (const [callId, { step, callSeq }] of pendingCalls) {
671
+ const started = callSeq !== void 0;
672
+ const message = freezeMessage({
673
+ id: MessageId(`interrupted-tool-result-${callId}-${seq}`),
674
+ role: "user",
675
+ source: {
676
+ kind: "tool",
677
+ callId
678
+ },
679
+ content: [{
680
+ type: "tool-result",
681
+ toolCallId: callId,
682
+ isError: true,
683
+ content: [{
684
+ type: "text",
685
+ text: started ? "The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly." : "The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed."
686
+ }]
687
+ }]
688
+ });
689
+ closers.push({
690
+ type: "tool/result",
691
+ seq: seq++,
692
+ time,
693
+ data: {
694
+ turn: openTurn,
695
+ step,
696
+ message,
697
+ error: started ? {
698
+ name: "ToolOutcomeUnknownError",
699
+ code: TOOL_OUTCOME_UNKNOWN
700
+ } : {
701
+ name: "ToolNotStartedError",
702
+ code: TOOL_NOT_STARTED
703
+ }
704
+ },
705
+ surfaceOp: "append",
706
+ ...started ? { sourceEventSeqs: [callSeq] } : {}
707
+ });
708
+ }
709
+ if (openStep !== null) closers.push({
710
+ type: "step/end",
711
+ seq: seq++,
712
+ time,
713
+ data: {
714
+ turn: openTurn,
715
+ step: openStep
716
+ }
717
+ });
718
+ closers.push({
719
+ type: "turn/end",
720
+ seq: seq++,
721
+ time,
722
+ data: {
723
+ turn: openTurn,
724
+ reason: { kind: "interrupted" }
725
+ }
726
+ });
727
+ return closers;
728
+ }
729
+ //#endregion
730
+ //#region lib/types/chunk-rows.js
731
+ /**
732
+ * Lossless storage packing for `assistant/chunk` delta runs. Providers stream
733
+ * token-sized deltas, so a log stores hundreds of near-identical event lines
734
+ * whose JSON envelopes dwarf their payloads (~56× measured on a real DeepSeek
735
+ * session). This module packs each run of consecutive same-block delta chunks
736
+ * into ONE storage row — `text-chunks`, `reasoning-chunks`, or
737
+ * `tool-call-chunks` — and expands rows back to the exact original events.
738
+ *
739
+ * Storage rows are a durable-encoding vocabulary, NOT session events: they
740
+ * never enter `Session.events`, have no `SessionEventMap` entry, and use bare
741
+ * (slash-less) type tags so a reader cannot confuse them with the event
742
+ * taxonomy (precedent: the JSONL header line's `session` tag). The encoder
743
+ * whitelists exact shapes — anything it does not fully recognize is stored
744
+ * verbatim, so unknown fields or future chunk variants lose compression, never
745
+ * data. The decoder validates before expanding and fails loud on a malformed
746
+ * row-tagged value instead of silently dropping a whole run.
747
+ *
748
+ * @module @deepseek-ai/dsh-session/chunk-rows
749
+ */
750
+ /**
751
+ * Minimum members before a run packs. Below it a row's envelope rivals the
752
+ * event lines it replaces. A format constant, not a tunable: both layouts
753
+ * decode identically, so changing it never invalidates stored logs.
754
+ */
755
+ const MIN_RUN = 3;
756
+ function isRecord(value) {
757
+ return typeof value === "object" && value !== null;
758
+ }
759
+ /** Exact-key check: `value` has every key in `keys` and nothing else. */
760
+ function hasExactKeys(value, keys) {
761
+ return Object.keys(value).length === keys.length && keys.every((k) => Object.hasOwn(value, k));
762
+ }
763
+ /**
764
+ * Classify an event for packing: its delta kind when the ENTIRE shape
765
+ * (envelope, data, chunk — exact keys, primitive types, integer seq/time) is
766
+ * whitelisted, else `undefined` (store verbatim). Inputs come from live typed
767
+ * appends AND parsed fixture files, so the checks are structural, not
768
+ * type-trusted. Integer times keep gap encoding exact: a fractional time would
769
+ * reconstruct through float subtraction/addition, which need not round-trip.
770
+ */
771
+ function classify(event) {
772
+ if (event.type !== "assistant/chunk") return void 0;
773
+ if (!hasExactKeys(event, [
774
+ "type",
775
+ "seq",
776
+ "time",
777
+ "data"
778
+ ])) return void 0;
779
+ if (!Number.isSafeInteger(event.seq) || event.seq < 0 || !Number.isSafeInteger(event.time)) return void 0;
780
+ const data = event.data;
781
+ if (!isRecord(data) || !hasExactKeys(data, [
782
+ "turn",
783
+ "step",
784
+ "chunk"
785
+ ])) return void 0;
786
+ if (typeof data.turn !== "number" || typeof data.step !== "number") return void 0;
787
+ const chunk = data.chunk;
788
+ if (!isRecord(chunk) || typeof chunk.index !== "number") return void 0;
789
+ switch (chunk.type) {
790
+ case "text-delta":
791
+ case "reasoning-delta": return hasExactKeys(chunk, [
792
+ "type",
793
+ "index",
794
+ "text"
795
+ ]) && typeof chunk.text === "string" ? chunk.type : void 0;
796
+ case "tool-call-delta": return (hasExactKeys(chunk, [
797
+ "type",
798
+ "index",
799
+ "id",
800
+ "argumentsDelta"
801
+ ]) || hasExactKeys(chunk, [
802
+ "type",
803
+ "index",
804
+ "id",
805
+ "name",
806
+ "argumentsDelta"
807
+ ]) && typeof chunk.name === "string") && typeof chunk.id === "string" && typeof chunk.argumentsDelta === "string" ? chunk.type : void 0;
808
+ default: return;
809
+ }
810
+ }
811
+ /** The tool-call fields of a whitelisted delta chunk (only after {@link classify} returned `'tool-call-delta'`). */
812
+ function toolCallOf(event) {
813
+ return event.data.chunk;
814
+ }
815
+ /** The block index of a whitelisted delta chunk (not every {@link StreamChunk} variant carries one). */
816
+ function indexOf(event) {
817
+ return event.data.chunk.index;
818
+ }
819
+ /** Whether `next` extends a run ending in `prev` (same kind already checked by the caller). */
820
+ function continues(prev, next, kind) {
821
+ if (next.seq !== prev.seq + 1) return false;
822
+ if (!Number.isSafeInteger(next.time - prev.time)) return false;
823
+ if (next.data.turn !== prev.data.turn || next.data.step !== prev.data.step) return false;
824
+ if (indexOf(next) !== indexOf(prev)) return false;
825
+ if (kind !== "tool-call-delta") return true;
826
+ const a = toolCallOf(prev);
827
+ const b = toolCallOf(next);
828
+ return a.id === b.id && Object.hasOwn(a, "name") === Object.hasOwn(b, "name") && a.name === b.name;
829
+ }
830
+ /** Build the row for a completed run (`run.length >= MIN_RUN`, uniform per {@link continues}). */
831
+ function buildRow(kind, run) {
832
+ const first = run[0];
833
+ const base = {
834
+ turn: first.data.turn,
835
+ step: first.data.step,
836
+ index: indexOf(first),
837
+ dt: run.slice(1).map((event, i) => event.time - run[i].time)
838
+ };
839
+ const envelope = {
840
+ seq0: first.seq,
841
+ time0: first.time
842
+ };
843
+ if (kind === "tool-call-delta") {
844
+ const call = toolCallOf(first);
845
+ return {
846
+ type: "tool-call-chunks",
847
+ ...envelope,
848
+ data: {
849
+ ...base,
850
+ id: CallId(call.id),
851
+ ...Object.hasOwn(call, "name") ? { name: call.name } : {},
852
+ args: run.map((event) => event.data.chunk.argumentsDelta)
853
+ }
854
+ };
855
+ }
856
+ const data = {
857
+ ...base,
858
+ texts: run.map((event) => event.data.chunk.text)
859
+ };
860
+ return kind === "text-delta" ? {
861
+ type: "text-chunks",
862
+ ...envelope,
863
+ data
864
+ } : {
865
+ type: "reasoning-chunks",
866
+ ...envelope,
867
+ data
868
+ };
869
+ }
870
+ /**
871
+ * Pack an event batch for storage: each run of at least {@link MIN_RUN}
872
+ * consecutive whitelisted same-kind, same-block delta chunk events becomes one
873
+ * {@link ChunkRow}; every other event passes through verbatim, in order.
874
+ * Pure and stateless — safe over any array, including a batch whose runs were
875
+ * split by flush boundaries (the split runs simply pack per batch).
876
+ *
877
+ * @param events - the batch to encode, in log order.
878
+ * @returns the storage records to write, one JSONL line each.
879
+ */
880
+ function packChunkRuns(events) {
881
+ const out = [];
882
+ let kind;
883
+ let run = [];
884
+ const flush = () => {
885
+ if (kind !== void 0 && run.length >= MIN_RUN) out.push(buildRow(kind, run));
886
+ else out.push(...run);
887
+ kind = void 0;
888
+ run = [];
889
+ };
890
+ for (const event of events) {
891
+ const k = classify(event);
892
+ if (k === void 0) {
893
+ flush();
894
+ out.push(event);
895
+ continue;
896
+ }
897
+ const delta = event;
898
+ const last = run[run.length - 1];
899
+ if (k === kind && last !== void 0 && continues(last, delta, k)) {
900
+ run.push(delta);
901
+ continue;
902
+ }
903
+ flush();
904
+ kind = k;
905
+ run = [delta];
906
+ }
907
+ flush();
908
+ return out;
909
+ }
910
+ /** Throw the uniform malformed-row diagnostic. */
911
+ function malformed(tag, why) {
912
+ throw new Error(`malformed ${tag} storage row: ${why}`);
913
+ }
914
+ /** Validate the shared run-data fields and the payload/dt arity; returns the member payload. */
915
+ function validateRunData(tag, data, payloadKey) {
916
+ if (typeof data.turn !== "number" || typeof data.step !== "number" || typeof data.index !== "number") malformed(tag, "turn/step/index must be numbers");
917
+ const payload = data[payloadKey];
918
+ if (!Array.isArray(payload) || payload.length === 0 || payload.some((entry) => typeof entry !== "string")) malformed(tag, `${payloadKey} must be a non-empty string array`);
919
+ const dt = data.dt;
920
+ if (!Array.isArray(dt) || dt.some((gap) => !Number.isSafeInteger(gap))) malformed(tag, "dt must be an array of safe integers");
921
+ if (dt.length !== payload.length - 1) malformed(tag, `dt length ${dt.length} does not match ${payload.length} members`);
922
+ return payload;
923
+ }
924
+ /** Validate a row-tagged parsed value's envelope and data, throwing on any malformation. */
925
+ function validateRow(value, tag) {
926
+ if (!hasExactKeys(value, [
927
+ "type",
928
+ "seq0",
929
+ "time0",
930
+ "data"
931
+ ])) malformed(tag, "envelope must be exactly {type, seq0, time0, data}");
932
+ if (!Number.isSafeInteger(value.seq0) || value.seq0 < 0) malformed(tag, "seq0 must be a non-negative safe integer");
933
+ if (!Number.isSafeInteger(value.time0)) malformed(tag, "time0 must be a safe integer");
934
+ const data = value.data;
935
+ if (!isRecord(data)) malformed(tag, "data must be an object");
936
+ let payload;
937
+ if (tag === "tool-call-chunks") {
938
+ const withName = hasExactKeys(data, [
939
+ "turn",
940
+ "step",
941
+ "index",
942
+ "id",
943
+ "name",
944
+ "dt",
945
+ "args"
946
+ ]);
947
+ if (!withName && !hasExactKeys(data, [
948
+ "turn",
949
+ "step",
950
+ "index",
951
+ "id",
952
+ "dt",
953
+ "args"
954
+ ])) malformed(tag, "data must be exactly {turn, step, index, id, name?, dt, args}");
955
+ if (typeof data.id !== "string" || withName && typeof data.name !== "string") malformed(tag, "id (and name when present) must be strings");
956
+ payload = validateRunData(tag, data, "args");
957
+ } else {
958
+ if (!hasExactKeys(data, [
959
+ "turn",
960
+ "step",
961
+ "index",
962
+ "dt",
963
+ "texts"
964
+ ])) malformed(tag, "data must be exactly {turn, step, index, dt, texts}");
965
+ payload = validateRunData(tag, data, "texts");
966
+ }
967
+ if (!Number.isSafeInteger(value.seq0 + payload.length - 1)) malformed(tag, "member seqs must stay safe integers");
968
+ let time = value.time0;
969
+ for (const gap of data.dt) {
970
+ time += gap;
971
+ if (!Number.isSafeInteger(time)) malformed(tag, "member times must stay safe integers");
972
+ }
973
+ return value;
974
+ }
975
+ /** Expand a validated row back into its exact original events, in order. */
976
+ function expandRow(row) {
977
+ const members = row.type === "tool-call-chunks" ? row.data.args : row.data.texts;
978
+ const events = [];
979
+ let time = row.time0;
980
+ for (let k = 0; k < members.length; k++) {
981
+ if (k > 0) time += row.data.dt[k - 1];
982
+ let chunk;
983
+ switch (row.type) {
984
+ case "text-chunks":
985
+ chunk = {
986
+ type: "text-delta",
987
+ index: row.data.index,
988
+ text: members[k]
989
+ };
990
+ break;
991
+ case "reasoning-chunks":
992
+ chunk = {
993
+ type: "reasoning-delta",
994
+ index: row.data.index,
995
+ text: members[k]
996
+ };
997
+ break;
998
+ case "tool-call-chunks":
999
+ chunk = {
1000
+ type: "tool-call-delta",
1001
+ index: row.data.index,
1002
+ id: row.data.id,
1003
+ ...Object.hasOwn(row.data, "name") ? { name: row.data.name } : {},
1004
+ argumentsDelta: members[k]
1005
+ };
1006
+ break;
1007
+ /* v8 ignore next 2 -- validateRow only returns the three row tags */
1008
+ default: return assertNever(row, "chunk-rows expandRow");
1009
+ }
1010
+ events.push({
1011
+ type: "assistant/chunk",
1012
+ seq: row.seq0 + k,
1013
+ time,
1014
+ data: {
1015
+ turn: row.data.turn,
1016
+ step: row.data.step,
1017
+ chunk
1018
+ }
1019
+ });
1020
+ }
1021
+ return events;
1022
+ }
1023
+ /**
1024
+ * Decode one parsed JSONL line value into the session event(s) it stores.
1025
+ * Chunk-row-tagged values validate and expand (a malformed row throws — it is
1026
+ * corrupt storage, and treating it as an event would silently drop a whole
1027
+ * run); every other value passes through as a single event, unvalidated.
1028
+ *
1029
+ * @param value - one line's `JSON.parse` result.
1030
+ * @returns the stored events, in log order.
1031
+ */
1032
+ function decodeStorageRecord(value) {
1033
+ if (!isRecord(value)) return [value];
1034
+ const tag = value.type;
1035
+ if (tag !== "text-chunks" && tag !== "reasoning-chunks" && tag !== "tool-call-chunks") return [value];
1036
+ return expandRow(validateRow(value, tag));
1037
+ }
1038
+ //#endregion
1039
+ //#region lib/types/index.js
1040
+ /**
1041
+ * Event-sourced session service: append-only session log, in-memory store, and
1042
+ * the derived LLM message history. Persistence is a plugin concern (subscribe
1043
+ * to `session/event`, drain on `session/flush`).
1044
+ *
1045
+ * @module @deepseek-ai/dsh-session
1046
+ */
1047
+ /**
1048
+ * Find the latest closed turn that entered at least one model step, ignoring
1049
+ * balanced no-step turns produced by rejection, empty input, or cancellation.
1050
+ * @param events - session events, or an owned suffix, to inspect.
1051
+ * @returns the latest matching turn end, or `undefined`.
1052
+ */
1053
+ function findLastMessageTurnEnd(events) {
1054
+ const steppedTurns = /* @__PURE__ */ new Set();
1055
+ let latest;
1056
+ for (const event of events) {
1057
+ if (event.type === "step/start") {
1058
+ steppedTurns.add(event.data.turn);
1059
+ continue;
1060
+ }
1061
+ if (event.type === "turn/end" && steppedTurns.delete(event.data.turn)) latest = event;
1062
+ }
1063
+ return latest;
1064
+ }
1065
+ /** Validate and freeze one detached creation header in place. */
1066
+ function validateSessionHeader(id, input) {
1067
+ if (input === null || typeof input !== "object" || Array.isArray(input)) throw new Error("session header is not a plain JSON record");
1068
+ const record = input;
1069
+ if (record.version !== 0) throw new Error(`session header version must be 0, got ${String(record.version)}`);
1070
+ if (record.id !== id) throw new Error(`session header id "${String(record.id)}" does not match session id "${id}"`);
1071
+ 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");
1072
+ if (record.cwd !== void 0) {
1073
+ if (typeof record.cwd !== "string") throw new Error("session header cwd must be a string");
1074
+ if (!isAbsolute(record.cwd)) throw new Error(`session header cwd must be an absolute path, got "${record.cwd}"`);
1075
+ }
1076
+ if (record.parentSession !== void 0 && typeof record.parentSession !== "string") throw new Error("session header parentSession must be a string");
1077
+ if (record.seedLength !== void 0 && (typeof record.seedLength !== "number" || !Number.isSafeInteger(record.seedLength) || record.seedLength < 0)) throw new Error("session header seedLength must be a non-negative safe integer");
1078
+ if (record.origin !== void 0 && record.origin !== "subagent") throw new Error("session header origin must be \"subagent\"");
1079
+ if (record.delegationDepth !== void 0 && (typeof record.delegationDepth !== "number" || !Number.isSafeInteger(record.delegationDepth) || record.delegationDepth < 0)) throw new Error("session header delegationDepth must be a non-negative safe integer");
1080
+ if (record.agentPreset !== void 0 && typeof record.agentPreset !== "string") throw new Error("session header agentPreset must be a string");
1081
+ return deepFreeze(record);
1082
+ }
1083
+ /** Validate and freeze one exclusively owned persistence header in place. */
1084
+ function validateRestoredSessionHeader(id, input) {
1085
+ if (input !== null && typeof input === "object" && !Array.isArray(input)) {
1086
+ const prototype = Reflect.getPrototypeOf(input);
1087
+ if (prototype !== Object.prototype && prototype !== null) throw new Error("session header is not a plain JSON record");
1088
+ }
1089
+ return validateSessionHeader(id, input);
1090
+ }
1091
+ /** Detach, validate, and freeze the creation metadata published by a session. */
1092
+ function snapshotSessionHeader(id, source) {
1093
+ const snapshot = snapshotJsonValue(source === void 0 ? {
1094
+ version: 0,
1095
+ id,
1096
+ createdAt: Date.now()
1097
+ } : source);
1098
+ if (snapshot === void 0) throw new Error("session header is not losslessly JSON-serializable");
1099
+ return validateSessionHeader(id, snapshot);
1100
+ }
1101
+ /**
1102
+ * Validate an exclusively owned event and deeply freeze its identified message
1103
+ * without copying the event. The caller transfers an object graph that no
1104
+ * producer retains and that shares no mutable children with another event.
1105
+ * Use {@link snapshotSessionEvent} when exclusive ownership is not guaranteed.
1106
+ * @param event - exclusively owned event imported across a trusted boundary.
1107
+ * @returns the same event object with a validated, deeply frozen message.
1108
+ */
1109
+ function adoptSessionEvent(event) {
1110
+ assertMessageEventShape(event, `session event at seq ${event.seq}`);
1111
+ switch (event.type) {
1112
+ case "user/message":
1113
+ deepFreeze(event.data);
1114
+ break;
1115
+ case "assistant/message":
1116
+ case "tool/result":
1117
+ deepFreeze(event.data.message);
1118
+ break;
1119
+ default: break;
1120
+ }
1121
+ return event;
1122
+ }
1123
+ /**
1124
+ * Detach one event while preserving deep immutability for its identified message.
1125
+ * @param event - event imported across a query or persistence boundary.
1126
+ * @returns a detached event snapshot with a validated, deeply frozen message.
1127
+ */
1128
+ function snapshotSessionEvent(event) {
1129
+ return adoptSessionEvent(structuredClone(event));
1130
+ }
1131
+ /** Deep-freeze one acyclic JSON tree without consuming the JavaScript call stack. */
1132
+ function freezeRestoredObject(value) {
1133
+ const pending = [value];
1134
+ while (pending.length > 0) {
1135
+ const current = pending.pop();
1136
+ Object.freeze(current);
1137
+ for (const key in current) {
1138
+ const child = current[key];
1139
+ if (child !== null && typeof child === "object") pending.push(child);
1140
+ }
1141
+ }
1142
+ return value;
1143
+ }
1144
+ /** Validate the fixed event envelope after one-pass JSON materialization. */
1145
+ function assertSessionEventEnvelope(value, index) {
1146
+ const event = value;
1147
+ if (event["type"] === "request/header-delta") throw new Error(`seed event at index ${index} uses unsupported legacy request/header-delta format`);
1148
+ for (const key in event) switch (key) {
1149
+ case "type":
1150
+ case "seq":
1151
+ case "time":
1152
+ case "data":
1153
+ case "surfaceOp":
1154
+ case "sourceEventSeqs": break;
1155
+ default: throw new Error(`seed event at index ${index} has an invalid event envelope`);
1156
+ }
1157
+ const type = event["type"];
1158
+ const seq = event["seq"];
1159
+ const time = event["time"];
1160
+ if (typeof type !== "string" || typeof seq !== "number" || !Number.isSafeInteger(seq) || seq < 0 || typeof time !== "number" || !Number.isSafeInteger(time) || event["data"] === void 0) throw new Error(`seed event at index ${index} has an invalid event envelope`);
1161
+ switch (type) {
1162
+ case "request/header":
1163
+ case "user/message":
1164
+ case "assistant/message":
1165
+ case "tool/result":
1166
+ assertCurrentLlmShape(event, index);
1167
+ break;
1168
+ }
1169
+ }
1170
+ /** Reject obsolete request headers and malformed messages at the seed/load boundary. */
1171
+ function assertCurrentLlmShape(event, index) {
1172
+ const data = event["data"];
1173
+ const record = typeof data === "object" && data !== null ? data : void 0;
1174
+ if (event["type"] === "request/header") {
1175
+ const header = record?.["header"];
1176
+ const headerRecord = typeof header === "object" && header !== null && !Array.isArray(header) ? header : void 0;
1177
+ const config = headerRecord?.["config"];
1178
+ if (!hasProviderModel(config)) throw new Error(`seed request/header at index ${index} lacks provider/model`);
1179
+ const configRecord = config;
1180
+ const reasoningEffort = configRecord["reasoningEffort"];
1181
+ if (reasoningEffort !== void 0 && (typeof reasoningEffort !== "string" || reasoningEffort.length === 0)) throw new Error(`seed request/header at index ${index} has an invalid reasoningEffort`);
1182
+ assertAdapterDefaults(headerRecord?.["adapterDefaults"], configRecord, index);
1183
+ }
1184
+ const type = event["type"];
1185
+ if (type !== "user/message" && type !== "assistant/message" && type !== "tool/result") return;
1186
+ assertMessageEventShape(event, `seed ${type} at index ${index}`);
1187
+ }
1188
+ const allowedAdapterKeys = new Set(["reasoningEffort", "maxTokens"]);
1189
+ /** Validate adapter-default markers imported from a durable request header. */
1190
+ function assertAdapterDefaults(value, config, index) {
1191
+ if (value === void 0) return;
1192
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`seed request/header at index ${index} has invalid adapterDefaults`);
1193
+ const defaults = value;
1194
+ 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`);
1195
+ }
1196
+ /** Validate only the event-specific invariants needed to safely replay a message. */
1197
+ function assertMessageEventShape(event, subject) {
1198
+ const type = event["type"];
1199
+ if (type !== "user/message" && type !== "assistant/message" && type !== "tool/result") return;
1200
+ const data = event["data"];
1201
+ const record = typeof data === "object" && data !== null ? data : void 0;
1202
+ const message = type === "user/message" ? record : record?.["message"];
1203
+ if (typeof message !== "object" || message === null || typeof message["id"] !== "string" || message["id"] === "") throw new Error(`${subject} lacks an identified message`);
1204
+ const messageRecord = message;
1205
+ const expectedRole = type === "assistant/message" ? "assistant" : "user";
1206
+ if (messageRecord["role"] !== expectedRole) throw new Error(`${subject} message must have role "${expectedRole}"`);
1207
+ const source = messageRecord["source"];
1208
+ if (typeof source !== "object" || source === null || typeof source["kind"] !== "string" || source["kind"] === "") throw new Error(`${subject} message has invalid source`);
1209
+ if (!Array.isArray(messageRecord["content"])) throw new Error(`${subject} message has invalid content`);
1210
+ const sourceRecord = source;
1211
+ if (type === "assistant/message") {
1212
+ if (sourceRecord["kind"] !== "model" || !hasProviderModel(sourceRecord)) throw new Error(`${subject} message must have model source`);
1213
+ return;
1214
+ }
1215
+ if (type !== "tool/result") return;
1216
+ if (sourceRecord["kind"] !== "tool" || typeof sourceRecord["callId"] !== "string" || sourceRecord["callId"] === "") throw new Error(`${subject} message must have tool source`);
1217
+ const content = messageRecord["content"];
1218
+ const block = content[0];
1219
+ if (content.length !== 1 || typeof block !== "object" || block === null || block["type"] !== "tool-result" || !Array.isArray(block["content"])) throw new Error(`${subject} message must contain one tool-result block`);
1220
+ if (block["toolCallId"] !== sourceRecord["callId"]) throw new Error(`${subject} message has mismatched tool call ids`);
1221
+ }
1222
+ /** Whether an unknown value carries the current provider/model pair. */
1223
+ function hasProviderModel(value) {
1224
+ if (typeof value !== "object" || value === null) return false;
1225
+ const pair = value;
1226
+ return typeof pair["provider"] === "string" && pair["provider"].length > 0 && typeof pair["model"] === "string" && pair["model"].length > 0;
1227
+ }
1228
+ /** Reject request-header vocabulary removed with the legacy delta codec. */
1229
+ function assertSupportedRequestHeader(type, data, location) {
1230
+ if (type === "request/header-delta") throw new Error(`${location} uses unsupported legacy request/header-delta format`);
1231
+ 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"`);
1232
+ }
1233
+ /** Resolve one listener snapshot, including Cordis's internal dispatch checks. */
1234
+ function collectSessionCallbacks(ctx, args) {
1235
+ return [...ctx.events.dispatch("emit", args)];
1236
+ }
1237
+ /** Invoke one resolved observe-only listener snapshot with per-listener containment. */
1238
+ function invokeContainedSessionObservers(ctx, name, id, args, callbacks) {
1239
+ for (const callback of callbacks) try {
1240
+ const returned = callback(...args);
1241
+ Promise.resolve(returned).catch((error) => {
1242
+ ctx.logger.warn(`session "${id}": ${name} listener rejected: ${String(error)}`);
1243
+ });
1244
+ } catch (error) {
1245
+ ctx.logger.warn(`session "${id}": ${name} listener threw: ${String(error)}`);
1246
+ }
1247
+ }
1248
+ /** Store attachment for the append path; module-private to keep Session store-agnostic publicly. */
1249
+ const attachments = /* @__PURE__ */ new WeakMap();
1250
+ /**
1251
+ * An event-sourced session: an append-only log of {@link SessionEvent}s.
1252
+ *
1253
+ * Plain class (not a Service) — create live instances via
1254
+ * `ctx.sessions.create()` and detached instances via {@link create}.
1255
+ * Seeding with an existing event log replays/forks a session.
1256
+ * @typert object
1257
+ */
1258
+ var Session = class Session {
1259
+ log = [];
1260
+ /** Single incremental owner of surface acceptance and projection state. */
1261
+ surfaceManager = new SurfaceManager(this.log);
1262
+ /** The ordered surface over this session's event log. */
1263
+ get surface() {
1264
+ return this.surfaceManager;
1265
+ }
1266
+ /**
1267
+ * Detached, deep-frozen creation metadata (format version, cwd, lineage,
1268
+ * seed boundary). Supplied by the store via `ctx.sessions.create()`. When a
1269
+ * `Session` is created without a store-owned header, a minimal header is
1270
+ * synthesized (stamped with the current {@link SESSION_FORMAT_VERSION}) so
1271
+ * `session.header` is always present. Kept out of the event log — it is a
1272
+ * storage concern, not replayable conversation state.
1273
+ */
1274
+ header;
1275
+ /** The session identity, derived from its durable header's single copy. */
1276
+ get id() {
1277
+ return this.header.id;
1278
+ }
1279
+ /**
1280
+ * The first seq appended IN THIS PROCESS: the length of the constructor
1281
+ * seed (0 without one). Events with smaller seq values entered through
1282
+ * construction — replay, fork, or resume — and were never published on the
1283
+ * `session/event` firehose (constructor seeds do not emit), so consumers
1284
+ * that replay the log as a publication substitute (telemetry adoption)
1285
+ * start here. Distinct from `header.seedLength`, the DURABLE fork-lineage
1286
+ * boundary: a resumed session's constructor seed is its full stored log,
1287
+ * while its header keeps the original fork value — this field is the
1288
+ * in-process construction fact.
1289
+ *
1290
+ * Not persisted itself: a seeded session projects it into the log as the
1291
+ * `session/end-seed` event, which is what a consumer reading STORED history
1292
+ * reads. Locate the LAST such event, not necessarily one at this seq — a
1293
+ * seed already ending in one is not re-marked, so reopening an untouched
1294
+ * session leaves that event at a smaller seq than `firstLiveSeq`. Prefer
1295
+ * this field in-process: it is exact before the marker reaches storage.
1296
+ *
1297
+ * When this lifecycle appends the marker, it occupies this seq before the
1298
+ * store attaches and therefore does not publish either. Otherwise this seq
1299
+ * holds an ordinary published write.
1300
+ */
1301
+ firstLiveSeq;
1302
+ /**
1303
+ * Create a detached session by validating and snapshotting borrowed seed
1304
+ * events and storage metadata.
1305
+ * @param id - session identity.
1306
+ * @param seed - optional borrowed replay or fork events.
1307
+ * @param header - optional borrowed storage metadata.
1308
+ * @returns a detached session.
1309
+ */
1310
+ static create(id, seed, header) {
1311
+ return new Session(id, seed, header);
1312
+ }
1313
+ /**
1314
+ * Restore a detached session by taking ownership of fresh persistence values.
1315
+ * The storage format, event envelopes, sequence continuity, surface transitions,
1316
+ * and header fields are validated before the restored objects are frozen.
1317
+ * @param id - restored session identity.
1318
+ * @param seed - fresh detached events whose ownership is transferred.
1319
+ * @param header - fresh detached metadata whose ownership is transferred.
1320
+ * @returns a restored detached session.
1321
+ */
1322
+ static fromRestore(id, seed, header) {
1323
+ return new Session(id, seed, header, "restore");
1324
+ }
1325
+ constructor(id, seed, header, mode = "snapshot") {
1326
+ const restoredHeader = mode === "restore" ? validateRestoredSessionHeader(id, header) : void 0;
1327
+ if (seed !== void 0) for (const [index, source] of seed.entries()) {
1328
+ const snapshot = mode === "restore" ? source : snapshotJsonValue(source);
1329
+ if (snapshot === void 0) throw new Error(`seed event at index ${index} is not losslessly JSON-serializable`);
1330
+ assertSessionEventEnvelope(snapshot, index);
1331
+ assertSupportedRequestHeader(snapshot.type, snapshot.data, `seed event at index ${index}`);
1332
+ if (snapshot.seq !== index) throw new Error(`seed event at index ${index} has seq ${snapshot.seq} (expected ${index}); seed must be contiguous from 0`);
1333
+ try {
1334
+ this.surfaceManager.validateNext(snapshot);
1335
+ } catch (error) {
1336
+ throw new Error(`invalid seed event at index ${index}: ${error instanceof Error ? error.message : "invalid surface metadata"}`);
1337
+ }
1338
+ this.log.push(mode === "restore" ? freezeRestoredObject(snapshot) : deepFreeze(snapshot));
1339
+ }
1340
+ this.firstLiveSeq = this.log.length;
1341
+ this.header = restoredHeader ?? snapshotSessionHeader(id, header);
1342
+ if (seed !== void 0 && this.log.at(-1)?.type !== "session/end-seed") this.append("session/end-seed", {});
1343
+ }
1344
+ /** Cached immutable public snapshot of the private append-only log. */
1345
+ eventsSnapshot;
1346
+ /**
1347
+ * An immutable snapshot of the append-only event log. The snapshot is reused
1348
+ * until the next append; a previously returned array does not grow later.
1349
+ * Events and their nested data are deep-frozen at acceptance, so neither a
1350
+ * cast nor ordinary JavaScript can rewrite durable history.
1351
+ */
1352
+ get events() {
1353
+ this.eventsSnapshot ??= Object.freeze([...this.log]);
1354
+ return this.eventsSnapshot;
1355
+ }
1356
+ /** The next event's sequence number — always the log length (the `seq = log.length` contiguity contract). */
1357
+ get seq() {
1358
+ return this.log.length;
1359
+ }
1360
+ /**
1361
+ * Append one typed event to the log and synchronously notify observers via
1362
+ * the store-owned, module-private publication hooks. The hot path never blocks
1363
+ * on I/O — persistence plugins buffer asynchronously. Once the event enters
1364
+ * the log, the append is committed: observer failures are logged and
1365
+ * contained per listener, so they do not change the return value or prevent
1366
+ * later listeners from observing the same accepted event.
1367
+ *
1368
+ * @param type - The event type (key of {@link SessionEventMap}).
1369
+ * @param data - The event payload; must be JSON-serializable.
1370
+ * @param opts - Surface metadata: `surfaceOp` controls how the event enters
1371
+ * the ordered surface; `sourceEventSeqs` lists the seq numbers of earlier
1372
+ * events this one derives from. REQUIRED for
1373
+ * {@link SurfaceEventType} events (every message-producing event must
1374
+ * declare how it joins the surface, the sole source of derived model
1375
+ * history) and
1376
+ * rejected by the compiler for non-surface types like `turn/start` or
1377
+ * `assistant/chunk`.
1378
+ * @returns the logged event — its assigned `seq`/`time` plus the SNAPSHOT of
1379
+ * `data` that entered the log, so reading `event.data` back sees the logged
1380
+ * value, never the caller's still-mutable input.
1381
+ * @throws if `data` or surface metadata is not losslessly JSON-serializable
1382
+ * (BigInt, function, symbol, undefined, negative zero, non-finite number,
1383
+ * circular reference, sparse array, or an exotic object such as
1384
+ * Map/Set/Date/class instance), or when the candidate violates the
1385
+ * canonical surface contract (marker shape and eligibility, unique
1386
+ * earlier source-event references, positional replacement validity, and complete
1387
+ * shadowed-node coverage). One recursive pass reads, validates, and
1388
+ * copies each nested value once, so a stateful getter cannot supply one value
1389
+ * to validation and another to storage. The event log is the durable source
1390
+ * of truth, so a bad event fails at the append site rather than later during
1391
+ * a backend flush. A synchronous internal dispatch validation failure or an
1392
+ * append reentered while this acceptance/publication boundary is open also
1393
+ * rejects before the log changes.
1394
+ */
1395
+ append(type, data, ...opts) {
1396
+ const surfaceOpts = opts[0];
1397
+ const surfaceMetadata = {
1398
+ ...surfaceOpts?.sourceEventSeqs === void 0 ? {} : { sourceEventSeqs: surfaceOpts.sourceEventSeqs },
1399
+ ...surfaceOpts?.surfaceOp === void 0 ? {} : { surfaceOp: surfaceOpts.surfaceOp }
1400
+ };
1401
+ const dataSnapshot = snapshotJsonValue(data);
1402
+ if (dataSnapshot === void 0) throw new Error(`session event "${type}" carries non-JSON-serializable data`);
1403
+ assertSupportedRequestHeader(type, dataSnapshot, `session event "${type}"`);
1404
+ const surfaceMetadataSnapshot = snapshotJsonValue(surfaceMetadata);
1405
+ if (surfaceMetadataSnapshot === void 0) throw new Error(`session event "${type}" carries non-JSON-serializable surface metadata`);
1406
+ const entry = attachments.get(this);
1407
+ if (entry?.appending) throw new Error("session append cannot reenter while another append is being published");
1408
+ const event = deepFreeze({
1409
+ type,
1410
+ seq: this.log.length,
1411
+ time: Date.now(),
1412
+ data: dataSnapshot,
1413
+ ...surfaceMetadataSnapshot
1414
+ });
1415
+ this.surfaceManager.validateNext(event);
1416
+ if (entry !== void 0) entry.appending = true;
1417
+ try {
1418
+ let callbacks;
1419
+ const callbackArgs = [this, event];
1420
+ if (entry !== void 0) callbacks = collectSessionCallbacks(entry.emitCtx, [
1421
+ entry.carrier,
1422
+ "session/event",
1423
+ ...callbackArgs
1424
+ ]);
1425
+ this.log.push(event);
1426
+ this.eventsSnapshot = void 0;
1427
+ if (callbacks !== void 0 && entry !== void 0) invokeContainedSessionObservers(entry.emitCtx, "session/event", entry.id, callbackArgs, callbacks);
1428
+ return event;
1429
+ } finally {
1430
+ if (entry !== void 0) {
1431
+ entry.appending = false;
1432
+ if (entry.detachRequested && !entry.announcing) entry.detach();
1433
+ }
1434
+ }
1435
+ }
1436
+ /** Cached fold of the request-header events — see {@link requestHeader}. */
1437
+ headerFold;
1438
+ /** Log position (events consumed) the header fold has reached. */
1439
+ headerFoldSeq = 0;
1440
+ /**
1441
+ * The {@link EpochHeader} in force after the log's last header event — the
1442
+ * header the NEXT request will be compared against — or undefined before
1443
+ * the first `request/header` snapshot. The live, incrementally-maintained
1444
+ * form of `foldRequestHeader(session.events)`: each header event is folded
1445
+ * once, when first seen, so a per-step read costs O(new events).
1446
+ * @returns the folded header, or undefined when no header event exists yet.
1447
+ */
1448
+ requestHeader() {
1449
+ if (this.headerFoldSeq < this.log.length) {
1450
+ this.headerFold = deepFreeze(foldRequestHeader(this.log.slice(this.headerFoldSeq), this.headerFold));
1451
+ this.headerFoldSeq = this.log.length;
1452
+ }
1453
+ return this.headerFold;
1454
+ }
1455
+ /** Cached fold of `request/context` events. */
1456
+ contextFold;
1457
+ contextFoldSeq = 0;
1458
+ /**
1459
+ * Return the latest resolved route metadata, or `undefined` before the first
1460
+ * `request/context` event. Each event is folded once.
1461
+ * @returns the latest immutable route metadata.
1462
+ */
1463
+ requestContext() {
1464
+ if (this.contextFoldSeq < this.log.length) {
1465
+ for (const event of this.log.slice(this.contextFoldSeq)) if (event.type === "request/context") this.contextFold = deepFreeze({ ...event.data });
1466
+ this.contextFoldSeq = this.log.length;
1467
+ }
1468
+ return this.contextFold;
1469
+ }
1470
+ /** The derived-message cache: frozen projections, extended per unseen node. */
1471
+ derived = [];
1472
+ /** Surface position (nodes projected) the cache has reached. */
1473
+ derivedNodes = 0;
1474
+ /** {@link SurfaceManager.replaceGeneration} the cache was built under. */
1475
+ derivedGeneration = 0;
1476
+ /**
1477
+ * Derive the LLM message history by walking the ordered sequences of
1478
+ * message-producing events maintained by `surfaceOp` markers. The
1479
+ * surface is the single source of derived history: every message-producing
1480
+ * append records its `surfaceOp`, so a raw event with no marker (a chunk, a
1481
+ * turn boundary) is correctly absent, and a compaction `replace` deletes the
1482
+ * shadowed nodes from the derivation. The projection rules are
1483
+ * {@link deriveEventMessage}, folded per node.
1484
+ *
1485
+ * CACHED: each surface node is projected exactly once, when first seen — a
1486
+ * call costs O(new nodes), and a surface rewrite (a `replace`;
1487
+ * {@link SessionSurface.replaceGeneration}) rebuilds. The returned array is
1488
+ * a fresh snapshot per call (later appends never grow an array a caller
1489
+ * already holds); the `Message` objects in it are SHARED and **deep-frozen**.
1490
+ * Their content reuses the already frozen durable event data, so the cache
1491
+ * needs no second deep clone and consumers still cannot mutate the log.
1492
+ * @returns a fresh array of the shared, frozen derived history.
1493
+ */
1494
+ deriveMessages() {
1495
+ const surface = this.surface;
1496
+ const nodes = surface.nodes;
1497
+ const generation = surface.replaceGeneration;
1498
+ if (generation !== this.derivedGeneration) {
1499
+ this.derived = [];
1500
+ this.derivedNodes = 0;
1501
+ this.derivedGeneration = generation;
1502
+ }
1503
+ for (const seq of nodes.slice(this.derivedNodes)) {
1504
+ const msg = this.deriveEventMessage(this.log[seq]);
1505
+ if (msg) this.derived.push(msg);
1506
+ }
1507
+ this.derivedNodes = nodes.length;
1508
+ return [...this.derived];
1509
+ }
1510
+ /**
1511
+ * Instance face of the pure per-node `deriveEventMessage` export from
1512
+ * `surface.ts`.
1513
+ * @param event - the event to project.
1514
+ * @returns the derived message, or null when the event produces none.
1515
+ */
1516
+ deriveEventMessage(event) {
1517
+ return deriveEventMessage(event);
1518
+ }
1519
+ };
1520
+ /** Typed error for session fork rejections. */
1521
+ var SessionForkError = class extends Error {
1522
+ code;
1523
+ constructor(message, code) {
1524
+ super(message);
1525
+ this.code = code;
1526
+ this.name = "SessionForkError";
1527
+ }
1528
+ };
1529
+ /**
1530
+ * In-memory session store (`ctx.sessions`).
1531
+ *
1532
+ * Persistence is intentionally not implemented here — persistence plugins
1533
+ * subscribe to `session/event` and flush on `session/flush` / dispose.
1534
+ */
1535
+ var SessionStore = class extends Service {
1536
+ store = /* @__PURE__ */ new Map();
1537
+ counter = 0;
1538
+ constructor(ctx) {
1539
+ super(ctx, "sessions");
1540
+ ctx.inject(["typert"], (typeCtx) => {
1541
+ typeCtx.typert.lookups.register("session", {
1542
+ parameter: "session",
1543
+ wire: "sessionId",
1544
+ hostTypeSymbol: "@deepseek-ai/dsh-session#Session",
1545
+ wireTypeSymbol: "@deepseek-ai/dsh-session/types#SessionId",
1546
+ resolve: (sessionId) => this.get(sessionId)
1547
+ });
1548
+ });
1549
+ }
1550
+ /**
1551
+ * Create a session owned by the calling fiber: disposing that fiber stops
1552
+ * event notification and removes the session from the store. `options.seed`
1553
+ * populates the session with a copy of those events (replay/fork);
1554
+ * `options.meta` attaches creation metadata (validated absolute `cwd`, seed
1555
+ * and parent lineage, and delegation depth) as the immutable
1556
+ * {@link SessionHeader} (the store fills `version`/`id`/`createdAt`).
1557
+ *
1558
+ * For an agent whose session must be torn down IN ORDER with its loop (so the
1559
+ * loop's final events are published before the store attachment ends), do NOT use this
1560
+ * — fold the session lifecycle into the agent's own effect via
1561
+ * {@link prepare} + {@link enter} + {@link announce} (see
1562
+ * `dsh-agent-loop`'s creation transaction).
1563
+ *
1564
+ * @param id - the session id; omitted, the store mints `session-<n>`.
1565
+ * @param options - seed events and/or creation metadata for the header.
1566
+ * @returns the live session, already entered and announced.
1567
+ * @throws if a session with `id` already exists, metadata is not a plain
1568
+ * lossless-JSON record with valid scalar fields, or `meta.cwd` is a
1569
+ * non-absolute path (storage backends key directories off it).
1570
+ */
1571
+ create(id, options) {
1572
+ const session = this.prepare(id, options);
1573
+ this.ctx.effect(function* () {
1574
+ yield this.enter(session);
1575
+ this.announce(session);
1576
+ }.bind(this), "sessions.create()");
1577
+ return session;
1578
+ }
1579
+ /**
1580
+ * Build a session WITHOUT entering it into the store — validate the id/cwd and
1581
+ * construct the {@link Session} (with its immutable {@link SessionHeader}).
1582
+ * Pairs with {@link enter} + {@link announce}: a caller that owns a composite
1583
+ * `ctx.effect` (the agent factory) folds the session lifecycle into that ONE
1584
+ * effect so a fiber unload tears the session + agent down as a single ORDERED
1585
+ * chain rather than as racing sibling effects — which would remove the publication hooks
1586
+ * before the driver's closing events commit, dropping them.
1587
+ *
1588
+ * @param id - the session id; omitted, the store mints `session-<n>`.
1589
+ * @param options - seed events and/or creation metadata for the header. With
1590
+ * `seedSource: 'persistence'`, metadata and events must be fresh detached
1591
+ * graphs whose ownership transfers to this call: they are validated and
1592
+ * frozen in place through {@link Session.fromRestore}, so the caller must
1593
+ * retain no mutable aliases.
1594
+ * @returns the constructed session, NOT yet in the store.
1595
+ * @throws if a session with `id` already exists, metadata is not a plain
1596
+ * lossless-JSON record with valid scalar fields, or `meta.cwd` is a
1597
+ * non-absolute path.
1598
+ */
1599
+ prepare(id, options) {
1600
+ let sessionId;
1601
+ if (id === void 0) do
1602
+ sessionId = SessionId(`session-${++this.counter}`);
1603
+ while (this.store.has(sessionId));
1604
+ else sessionId = SessionId(id);
1605
+ if (this.store.has(sessionId)) throw new Error(`session "${sessionId}" already exists`);
1606
+ if (options?.seedSource === "persistence") return Session.fromRestore(sessionId, options.seed, options.meta);
1607
+ const seed = options?.seed;
1608
+ const meta = options?.meta;
1609
+ const header = {
1610
+ version: 0,
1611
+ id: sessionId,
1612
+ createdAt: meta?.createdAt ?? Date.now(),
1613
+ ...meta?.cwd === void 0 ? {} : { cwd: meta.cwd },
1614
+ ...meta?.parentSession === void 0 ? {} : { parentSession: meta.parentSession },
1615
+ ...meta?.seedLength === void 0 ? {} : { seedLength: meta.seedLength },
1616
+ ...meta?.origin === void 0 ? {} : { origin: meta.origin },
1617
+ ...meta?.delegationDepth === void 0 ? {} : { delegationDepth: meta.delegationDepth },
1618
+ ...meta?.agentPreset === void 0 ? {} : { agentPreset: meta.agentPreset }
1619
+ };
1620
+ return Session.create(sessionId, seed, header);
1621
+ }
1622
+ /**
1623
+ * Enter a {@link prepare}d session into the store: install the module-private
1624
+ * append publication hooks and add it to the store. Returns the DETACH
1625
+ * disposer (hooks + store removal). Does NOT emit `session/created` —
1626
+ * the caller yields this disposer inside its effect and THEN calls
1627
+ * {@link announce}, so a throwing `session/created` listener rolls the attach
1628
+ * back instead of leaking it.
1629
+ *
1630
+ * Re-checks the id for a duplicate: `prepare` and `enter` are public
1631
+ * cross-package primitives and a caller may interleave arbitrary work (or
1632
+ * another create) between them, so a stale prepared session must NOT overwrite
1633
+ * a live store entry of the same id — its detach disposer would later delete
1634
+ * the REAL session. The {@link create} convenience and the agent factory call
1635
+ * the two back-to-back so they never trip this, but the public API cannot
1636
+ * assume that.
1637
+ *
1638
+ * @param session - a {@link prepare}d session not yet in the store.
1639
+ * @returns the detach disposer (publication hooks + store removal). When called from
1640
+ * a synchronous `session/created` listener, removal and disposal wait until
1641
+ * that creation dispatch unwinds.
1642
+ * @throws if a session with this id is already in the store.
1643
+ */
1644
+ enter(session) {
1645
+ const id = session.id;
1646
+ const carrier = scopeTarget(session, scopeOf(this.ctx));
1647
+ if (this.store.has(id)) throw new Error(`session "${id}" already exists`);
1648
+ if (attachments.has(session)) throw new Error(`session "${id}" is already attached to a store`);
1649
+ const entry = {
1650
+ id,
1651
+ session,
1652
+ carrier,
1653
+ emitCtx: this.ctx,
1654
+ announced: false,
1655
+ announcing: false,
1656
+ appending: false,
1657
+ detachRequested: false,
1658
+ detach: () => {
1659
+ this.detachEntered(entry);
1660
+ }
1661
+ };
1662
+ this.store.set(id, entry);
1663
+ attachments.set(session, entry);
1664
+ let entered = true;
1665
+ const detach = () => {
1666
+ if (!entered) return;
1667
+ entered = false;
1668
+ if (entry.announcing || entry.appending) {
1669
+ entry.detachRequested = true;
1670
+ return;
1671
+ }
1672
+ entry.detach();
1673
+ };
1674
+ return detach;
1675
+ }
1676
+ /** Remove one exact entered session and emit its paired disposal when announced. */
1677
+ detachEntered(entry) {
1678
+ entry.detachRequested = false;
1679
+ /* v8 ignore next -- enter() rejects replacement while this single-shot detach capability is live. */
1680
+ if (this.store.get(entry.id) !== entry) return;
1681
+ this.store.delete(entry.id);
1682
+ attachments.delete(entry.session);
1683
+ if (entry.announced) this.emitDisposed(entry);
1684
+ }
1685
+ /** Emit `session/created` exactly once for an {@link enter}ed session (with
1686
+ * the carrier {@link enter} captured). Separate from {@link enter} so the
1687
+ * caller can yield the detach disposer first (rollback safety — see
1688
+ * {@link enter}).
1689
+ * @param session - the entered session to announce to listeners.
1690
+ * @throws if the session is not live or its announcement already began,
1691
+ * including a reentrant call from a creation listener. */
1692
+ announce(session) {
1693
+ const entry = this.liveEntryFor(session);
1694
+ if (entry.announced || entry.announcing) throw new Error(`session "${entry.id}" was already announced`);
1695
+ entry.announced = true;
1696
+ const callbackArgs = [session];
1697
+ entry.announcing = true;
1698
+ try {
1699
+ const callbacks = collectSessionCallbacks(this.ctx, [
1700
+ entry.carrier,
1701
+ "session/created",
1702
+ session
1703
+ ]);
1704
+ for (const callback of callbacks) {
1705
+ const returned = callback(...callbackArgs);
1706
+ Promise.resolve(returned).catch((error) => {
1707
+ this.ctx.logger.warn(`session "${entry.id}": session/created listener rejected: ${String(error)}`);
1708
+ });
1709
+ }
1710
+ } finally {
1711
+ entry.announcing = false;
1712
+ if (entry.detachRequested && !entry.appending) entry.detach();
1713
+ }
1714
+ }
1715
+ /** Emit the paired teardown notification with per-listener containment. */
1716
+ emitDisposed(entry) {
1717
+ const callbackArgs = [entry.session];
1718
+ try {
1719
+ const callbacks = collectSessionCallbacks(this.ctx, [
1720
+ entry.carrier,
1721
+ "session/disposed",
1722
+ entry.session
1723
+ ]);
1724
+ invokeContainedSessionObservers(this.ctx, "session/disposed", entry.id, callbackArgs, callbacks);
1725
+ } catch (error) {
1726
+ this.ctx.logger.warn(`session "${entry.id}": session/disposed dispatch threw: ${String(error)}`);
1727
+ }
1728
+ }
1729
+ /**
1730
+ * Dispatch the awaited `session/flush` durability checkpoint for `session`,
1731
+ * with the carrier captured at {@link enter}. THE flush entry point: the
1732
+ * store owns the carrier, so callers (the checkpoint policy's per-request
1733
+ * barrier, goal-session's idle checkpoint, teardown drains, and consumers
1734
+ * that flush themselves before reading storage) must come through here
1735
+ * rather than dispatch a raw `ctx.parallel('session/flush', …)` — one owner,
1736
+ * one spelling, and the scoped-dispatch invariant can pin it.
1737
+ * @param session - the session whose buffered events must reach durable storage.
1738
+ * @returns whether at least one durability listener participated, after every
1739
+ * listener has settled successfully.
1740
+ * @throws the first registered listener failure after every listener settles.
1741
+ */
1742
+ async flush(session) {
1743
+ const { carrier } = this.liveEntryFor(session);
1744
+ const callbackArgs = [session];
1745
+ const callbacks = collectSessionCallbacks(this.ctx, [
1746
+ carrier,
1747
+ "session/flush",
1748
+ session
1749
+ ]);
1750
+ const failure = (await Promise.allSettled(callbacks.map((callback) => {
1751
+ try {
1752
+ return callback(...callbackArgs);
1753
+ } catch (error) {
1754
+ return Promise.reject(error);
1755
+ }
1756
+ }))).find((result) => result.status === "rejected");
1757
+ if (failure !== void 0) throw failure.reason;
1758
+ return callbacks.length > 0;
1759
+ }
1760
+ /** Return the exact live entry; detached/prepared objects reject. */
1761
+ liveEntryFor(session) {
1762
+ const entry = attachments.get(session);
1763
+ if (entry === void 0 || this.store.get(entry.id) !== entry) throw new Error(`session "${session.id}" is not live in this store`);
1764
+ return entry;
1765
+ }
1766
+ /**
1767
+ * Look up a live session.
1768
+ * @param id - the session id to look up.
1769
+ * @returns the session, or undefined when no live session has that id.
1770
+ */
1771
+ get(id) {
1772
+ return this.store.get(id)?.session;
1773
+ }
1774
+ /**
1775
+ * All live sessions, in creation order.
1776
+ * @returns a fresh array; mutating it does not affect the store.
1777
+ */
1778
+ list() {
1779
+ return [...this.store.values()].map((entry) => entry.session);
1780
+ }
1781
+ /**
1782
+ * Create a live child session from a stable prefix of a live source.
1783
+ * `boundary` is an inclusive source event seq; omitted means the source's
1784
+ * current last event. The selected slice may end with a between-turn event
1785
+ * but must not end inside an open turn.
1786
+ *
1787
+ * @param source - Live source session object or id.
1788
+ * @param boundary - Inclusive source event seq to fork through; omitted means
1789
+ * the source's current last event, and omitted on an empty source forks an
1790
+ * empty child.
1791
+ * @param childSessionId - Optional child session id; omitted delegates to
1792
+ * `SessionStore`'s id policy.
1793
+ * @returns The created live child session.
1794
+ */
1795
+ fork(source, boundary, childSessionId) {
1796
+ if (childSessionId !== void 0 && this.get(childSessionId) !== void 0) throw new SessionForkError(`session "${childSessionId}" already exists`, "SESSION_ALREADY_EXISTS");
1797
+ const liveSource = this._resolveForkSource(source);
1798
+ const seed = this._forkSeed(liveSource, boundary);
1799
+ return this.create(childSessionId, {
1800
+ seed,
1801
+ meta: {
1802
+ ...liveSource.header.cwd !== void 0 ? { cwd: liveSource.header.cwd } : {},
1803
+ parentSession: liveSource.id,
1804
+ seedLength: seed.length
1805
+ }
1806
+ });
1807
+ }
1808
+ _forkSeed(session, requestedBoundary) {
1809
+ const events = session.events;
1810
+ const lastEvent = events.at(-1);
1811
+ let boundary;
1812
+ if (requestedBoundary !== void 0) boundary = requestedBoundary;
1813
+ else {
1814
+ if (lastEvent === void 0) return [];
1815
+ boundary = lastEvent.seq;
1816
+ }
1817
+ if (!Number.isSafeInteger(boundary) || boundary < 0) throw new SessionForkError(`fork boundary for session "${session.id}" must be a non-negative safe integer, got ${String(boundary)}`, "INVALID_BOUNDARY");
1818
+ if (boundary >= events.length) {
1819
+ const lastSeq = events.at(-1)?.seq;
1820
+ throw new SessionForkError(`fork boundary ${boundary} does not exist in session "${session.id}" (last seq: ${lastSeq ?? "none"})`, "INVALID_BOUNDARY");
1821
+ }
1822
+ const boundaryEvent = events[boundary];
1823
+ if (boundaryEvent === void 0 || boundaryEvent.seq !== boundary) throw new SessionForkError(`fork boundary ${boundary} does not match a contiguous event seq in session "${session.id}"`, "INVALID_BOUNDARY");
1824
+ const lastTurnBoundary = events.slice(0, boundary + 1).findLast((event) => event.type === "turn/start" || event.type === "turn/end");
1825
+ if (lastTurnBoundary?.type === "turn/start") throw new SessionForkError(`fork boundary ${boundary} in session "${session.id}" ends inside open turn ${lastTurnBoundary.data.turn}`, "OPEN_TURN");
1826
+ return events.slice(0, boundary + 1);
1827
+ }
1828
+ _resolveForkSource(source) {
1829
+ if (typeof source === "string") {
1830
+ const session = this.get(source);
1831
+ if (session === void 0) throw new SessionForkError(`session "${source}" not found`, "SESSION_NOT_FOUND");
1832
+ return session;
1833
+ }
1834
+ const live = this.get(source.id);
1835
+ if (live === void 0) throw new SessionForkError(`session "${source.id}" not found`, "SESSION_NOT_FOUND");
1836
+ if (live !== source) throw new SessionForkError(`session "${source.id}" is not the live store instance`, "SESSION_NOT_LIVE");
1837
+ return source;
1838
+ }
1839
+ };
1840
+ //#endregion
1841
+ export { SESSION_FORMAT_VERSION, Session, SessionForkError, SessionId, SessionPreparation, SessionStore, SessionStore as default, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN, adoptSessionEvent, canonicalHeader, decodeStorageRecord, deriveEventMessage, findLastMessageTurnEnd, foldRequestHeader, foldSurface, headerEquals, interruptedTurnClosers, isAppendSurfaceEvent, isJsonValue, isReplacementSurfaceEvent, isSurfaceEligibleType, isSurfaceEvent, lastActivityTime, packChunkRuns, snapshotJsonValue, snapshotSessionEvent };