@deepseek-ai/dsh-session 0.1.6-alpha.2 → 0.1.7-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +11 -9
- package/README.zh.md +12 -8
- package/lib/index.js +215 -125
- package/lib/invariant.js +4 -1
- package/lib/types/fork.d.ts +18 -0
- package/lib/types/fork.js +28 -0
- package/lib/types/index.d.ts +27 -29
- package/lib/types/index.js +56 -61
- package/lib/types/invariant.js +5 -1
- package/lib/types/known-event-types.js +1 -0
- package/lib/types/repair.d.ts +42 -7
- package/lib/types/repair.js +52 -23
- package/lib/types/surface.d.ts +4 -4
- package/lib/types/surface.js +73 -17
- package/lib/types/types.d.ts +38 -15
- package/lib/types/types.js +1 -1
- package/package.json +15 -11
package/lib/index.js
CHANGED
|
@@ -53,7 +53,7 @@ function SessionLogOffset(value) {
|
|
|
53
53
|
* immutable prior-generation, and current fast-path rules are recorded in
|
|
54
54
|
* `.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md`.
|
|
55
55
|
*/
|
|
56
|
-
const SESSION_FORMAT_VERSION =
|
|
56
|
+
const SESSION_FORMAT_VERSION = 4;
|
|
57
57
|
//#endregion
|
|
58
58
|
//#region lib/types/known-event-types.js
|
|
59
59
|
/**
|
|
@@ -91,6 +91,7 @@ const KNOWN_SESSION_EVENT_TYPES = new Set([
|
|
|
91
91
|
"compaction/start",
|
|
92
92
|
"compaction/summary",
|
|
93
93
|
"deliverables/presented",
|
|
94
|
+
"developer/message",
|
|
94
95
|
"feedback/message-delete",
|
|
95
96
|
"feedback/message-put",
|
|
96
97
|
"feedback/record",
|
|
@@ -152,6 +153,7 @@ const MESSAGE_PROJECTION_EVENT_TYPES = new Set(["image/offload"]);
|
|
|
152
153
|
/** Runtime counterpart of the message-producing event union. */
|
|
153
154
|
const SURFACE_EVENT_TYPES = new Set([
|
|
154
155
|
"system/message",
|
|
156
|
+
"developer/message",
|
|
155
157
|
"user/message",
|
|
156
158
|
"assistant/message",
|
|
157
159
|
"tool/result"
|
|
@@ -159,7 +161,7 @@ const SURFACE_EVENT_TYPES = new Set([
|
|
|
159
161
|
/**
|
|
160
162
|
* Whether an event type can join the model-visible surface.
|
|
161
163
|
* @param type - event type to test.
|
|
162
|
-
* @returns true for one of the
|
|
164
|
+
* @returns true for one of the message-producing event types.
|
|
163
165
|
*/
|
|
164
166
|
function isSurfaceEligibleType(type) {
|
|
165
167
|
return SURFACE_EVENT_TYPES.has(type);
|
|
@@ -200,7 +202,7 @@ function isReplacementSurfaceEvent(event) {
|
|
|
200
202
|
/**
|
|
201
203
|
* Project a single event into the LLM message it derives to, or null when it
|
|
202
204
|
* produces none — a non-surface event (attempt, boundary, log-only record) or an
|
|
203
|
-
* empty-content
|
|
205
|
+
* empty-content system, developer, or assistant message. A caller
|
|
204
206
|
* reconstructing model input supplies the same prefix's `projectedMessages`
|
|
205
207
|
* from {@link foldSurface}; without that map this function reads original
|
|
206
208
|
* event content. Session instance methods apply the live projection. Messages
|
|
@@ -215,6 +217,7 @@ function deriveEventMessage(event, projectedMessages) {
|
|
|
215
217
|
switch (event.type) {
|
|
216
218
|
case "user/message": return event.data;
|
|
217
219
|
case "system/message":
|
|
220
|
+
case "developer/message":
|
|
218
221
|
case "assistant/message":
|
|
219
222
|
if (event.data.message.content.length === 0) return null;
|
|
220
223
|
return event.data.message;
|
|
@@ -227,14 +230,33 @@ function isRecord(value) {
|
|
|
227
230
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
228
231
|
}
|
|
229
232
|
/**
|
|
230
|
-
* Reject noncanonical request-header fields and contradictory tool failure metadata.
|
|
233
|
+
* Reject noncanonical request-header fields, developer roles/content, and contradictory tool failure metadata.
|
|
231
234
|
* This does not validate complete event payloads or embedded provider streams.
|
|
232
235
|
* @param event - event whose locally related payload fields are inspected.
|
|
233
236
|
* @param subject - event location to include in validation errors.
|
|
234
|
-
* @throws when request
|
|
237
|
+
* @throws when request-header fields, developer roles/content, or tool failure metadata are invalid.
|
|
235
238
|
*/
|
|
236
239
|
function validateSessionEventData(event, subject) {
|
|
237
240
|
const data = event.data;
|
|
241
|
+
if (SURFACE_EVENT_TYPES.has(event.type) && isRecord(data)) {
|
|
242
|
+
const message = event.type === "user/message" ? data : data["message"];
|
|
243
|
+
if (isRecord(message)) {
|
|
244
|
+
if (event.type === "developer/message" !== (message["role"] === "developer")) throw new Error(`${subject} developer/message and developer role must occur together`);
|
|
245
|
+
if (message["role"] !== "developer" && Array.isArray(message["content"]) && message["content"].some((block) => isRecord(block) && (block["type"] === "tool-addition" || block["type"] === "tool-removal"))) throw new Error(`${subject} tool-change blocks require developer role`);
|
|
246
|
+
if (event.type === "developer/message" && Array.isArray(message["content"])) {
|
|
247
|
+
let hasAdditions = false;
|
|
248
|
+
for (const block of message["content"]) {
|
|
249
|
+
if (!isRecord(block) || block["type"] !== "tool-addition" && block["type"] !== "tool-removal") continue;
|
|
250
|
+
if (typeof block["toolName"] !== "string" || block["toolName"].length === 0) throw new Error(`${subject} ${block["type"]} requires a nonempty toolName`);
|
|
251
|
+
if (block["type"] === "tool-addition") {
|
|
252
|
+
hasAdditions = true;
|
|
253
|
+
if (Object.hasOwn(block, "tool")) throw new Error(`${subject} tool-addition must omit inline tool definitions`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (hasAdditions ? !isEventSeq(data["headerSeq"]) : Object.hasOwn(data, "headerSeq")) throw new Error(`${subject} requires headerSeq exactly when tool additions are present`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
238
260
|
if (event.type === "request/header") {
|
|
239
261
|
if (!isRecord(data)) throw new Error(`${subject} data must be an object`);
|
|
240
262
|
const header = data["header"];
|
|
@@ -247,9 +269,7 @@ function validateSessionEventData(event, subject) {
|
|
|
247
269
|
if (!isRecord(data)) throw new Error(`${subject} data must be an object`);
|
|
248
270
|
if (data["error"] === void 0) return;
|
|
249
271
|
const message = data["message"];
|
|
250
|
-
|
|
251
|
-
const block = Array.isArray(content) ? content[0] : void 0;
|
|
252
|
-
if (!isRecord(block) || block["isError"] !== true) throw new Error(`${subject} error requires message content[0].isError === true`);
|
|
272
|
+
if (!isRecord(message) || message["isError"] !== true) throw new Error(`${subject} error requires message.isError === true`);
|
|
253
273
|
}
|
|
254
274
|
}
|
|
255
275
|
/** Create an empty surface fold state. */
|
|
@@ -307,6 +327,23 @@ function assertSourceEventReferences(event, shadowedSeqs) {
|
|
|
307
327
|
const missing = shadowedSeqs.filter((seq) => !sources.has(seq));
|
|
308
328
|
if (missing.length > 0) throw new Error(`surface replace: sourceEventSeqs must include every shadowed surface node; missing ${missing.join(", ")}`);
|
|
309
329
|
}
|
|
330
|
+
/** Resolve tool additions against their immutable historical request header. */
|
|
331
|
+
function assertDeveloperHeader(event, events, baseSeq) {
|
|
332
|
+
if (event.type !== "developer/message") return;
|
|
333
|
+
validateSessionEventData(event, `developer/message at seq ${event.seq}`);
|
|
334
|
+
if (event.data.headerSeq === void 0) return;
|
|
335
|
+
const headerSeq = event.data.headerSeq;
|
|
336
|
+
const headerEvent = events[headerSeq - baseSeq];
|
|
337
|
+
if (headerSeq >= event.seq || headerEvent?.type !== "request/header") throw new Error("developer/message headerSeq must reference an earlier request/header");
|
|
338
|
+
for (const block of event.data.message.content) {
|
|
339
|
+
if (block.type !== "tool-addition") continue;
|
|
340
|
+
const definitions = headerEvent.data.header.tools?.filter((tool) => tool.name === block.toolName) ?? [];
|
|
341
|
+
if (definitions.length !== 1) throw new Error(`developer/message tool-addition "${block.toolName}" must name exactly one tool in headerSeq ${headerSeq}`);
|
|
342
|
+
const definition = definitions[0];
|
|
343
|
+
if (typeof definition.description !== "string" || !isRecord(definition.parameters)) throw new Error(`developer/message tool-addition "${block.toolName}" requires a complete tool definition in headerSeq ${headerSeq}`);
|
|
344
|
+
if (Object.hasOwn(definition, "deferLoading") && definition.deferLoading !== true) throw new Error("developer/message referenced tool deferLoading must be true when present");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
310
347
|
/**
|
|
311
348
|
* Validate one event's surface metadata without checking membership in a log or surface.
|
|
312
349
|
* @param event - event whose marker and source sequence values are inspected.
|
|
@@ -359,21 +396,13 @@ function assertToolResultRewrite(event, shadowedSeqs, events, baseSeq) {
|
|
|
359
396
|
if (original?.type !== "tool/result") throw new Error("tool/result surface replacement must target a current tool/result");
|
|
360
397
|
const originalRest = { ...original.data };
|
|
361
398
|
const replacementRest = { ...event.data };
|
|
362
|
-
const originalResult = original.data.message.content[0];
|
|
363
|
-
const replacementResult = event.data.message.content[0];
|
|
364
399
|
originalRest["message"] = {
|
|
365
400
|
...original.data.message,
|
|
366
|
-
content:
|
|
367
|
-
...originalResult,
|
|
368
|
-
content: null
|
|
369
|
-
}]
|
|
401
|
+
content: null
|
|
370
402
|
};
|
|
371
403
|
replacementRest["message"] = {
|
|
372
404
|
...event.data.message,
|
|
373
|
-
content:
|
|
374
|
-
...replacementResult,
|
|
375
|
-
content: null
|
|
376
|
-
}]
|
|
405
|
+
content: null
|
|
377
406
|
};
|
|
378
407
|
if (!isDeepEqualJson(originalRest, replacementRest)) throw new Error("tool/result surface replacement may change only content");
|
|
379
408
|
}
|
|
@@ -393,6 +422,7 @@ function assertSystemHeadRewrite(event, state, startIdx, shadowedSeqs, events, b
|
|
|
393
422
|
function planSurfaceEvent(state, event, expectedSeq, events, baseSeq, projections) {
|
|
394
423
|
if (event.seq !== expectedSeq) throw new Error(`session event seq ${event.seq} is not contiguous; expected ${expectedSeq}`);
|
|
395
424
|
const surfaceOp = validateSurfaceMetadata(event);
|
|
425
|
+
assertDeveloperHeader(event, events, baseSeq);
|
|
396
426
|
const projection = projections.find((item) => item.type === event.type);
|
|
397
427
|
if (projection !== void 0) return {
|
|
398
428
|
kind: "project",
|
|
@@ -607,64 +637,49 @@ function foldRequestHeader(events, from) {
|
|
|
607
637
|
return state;
|
|
608
638
|
}
|
|
609
639
|
//#endregion
|
|
610
|
-
//#region lib/types/preparation.js
|
|
611
|
-
/**
|
|
612
|
-
* Ownership of one unpublished Session before registry publication.
|
|
613
|
-
* @module @deepseek-ai/dsh-session/preparation
|
|
614
|
-
*/
|
|
615
|
-
/**
|
|
616
|
-
* One exact unpublished Session and the provider state that keeps it usable.
|
|
617
|
-
* Disposal is synchronous and idempotent. Providers decide whether release
|
|
618
|
-
* returns the Session to a cache or discards it; publication may consume that
|
|
619
|
-
* state before disposal, making the callback a no-op.
|
|
620
|
-
*/
|
|
621
|
-
var SessionPreparation = class SessionPreparation {
|
|
622
|
-
options;
|
|
623
|
-
released = false;
|
|
624
|
-
/** The exact Session to use for setup and publication. */
|
|
625
|
-
session;
|
|
626
|
-
constructor(session, options) {
|
|
627
|
-
this.options = options;
|
|
628
|
-
this.session = session;
|
|
629
|
-
}
|
|
630
|
-
/**
|
|
631
|
-
* Wrap an unpublished Session in one preparation lifetime.
|
|
632
|
-
* @param session - exact unpublished Session.
|
|
633
|
-
* @param options - optional provider release behavior.
|
|
634
|
-
* @returns a preparation disposed after publication or rollback.
|
|
635
|
-
*/
|
|
636
|
-
static create(session, options) {
|
|
637
|
-
return new SessionPreparation(session, options ?? {});
|
|
638
|
-
}
|
|
639
|
-
/** Release provider state once when this preparation leaves its caller. */
|
|
640
|
-
[Symbol.dispose]() {
|
|
641
|
-
if (this.released) return;
|
|
642
|
-
this.released = true;
|
|
643
|
-
this.options.release?.();
|
|
644
|
-
}
|
|
645
|
-
};
|
|
646
|
-
//#endregion
|
|
647
640
|
//#region lib/types/repair.js
|
|
648
641
|
/**
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
642
|
+
* Synthetic closer events that balance a session log whose tail turn is open.
|
|
643
|
+
* Two producers share the mechanism: crash recovery closes an interrupted
|
|
644
|
+
* persisted log on reload, and fork-seed construction closes a prefix cut
|
|
645
|
+
* inside the source's open turn. Both preserve every fully written event and
|
|
646
|
+
* close the unfinished step and turn. Calls in already closed steps remain
|
|
647
|
+
* unchanged, including any missing results.
|
|
652
648
|
* @module @deepseek-ai/dsh-session/repair
|
|
653
649
|
*/
|
|
654
650
|
/** Recovery code for an assistant tool request that never reached a recorded call start. */
|
|
655
651
|
const TOOL_NOT_STARTED = "TOOL_NOT_STARTED";
|
|
656
652
|
/** Recovery code for a recorded tool call whose completed outcome was not durably recorded. */
|
|
657
653
|
const TOOL_OUTCOME_UNKNOWN = "TOOL_OUTCOME_UNKNOWN";
|
|
654
|
+
/** Model-visible wording of the synthetic error tool results, keyed by cause. */
|
|
655
|
+
const CLOSER_TEXT = {
|
|
656
|
+
interrupted: {
|
|
657
|
+
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.",
|
|
658
|
+
notStarted: "The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed."
|
|
659
|
+
},
|
|
660
|
+
forked: {
|
|
661
|
+
started: "The history inherited by this branch records this tool call starting but does not include its result. The parent session may have completed it after the fork point. 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.",
|
|
662
|
+
notStarted: "The history inherited by this branch has no record of this tool call starting. The parent session may have executed it after the fork point. 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."
|
|
663
|
+
}
|
|
664
|
+
};
|
|
658
665
|
/**
|
|
659
666
|
* Return deterministic synthetic events that close an open tail turn. Unmatched
|
|
660
|
-
* calls receive error results
|
|
661
|
-
*
|
|
662
|
-
* last real event. A balanced or empty log returns no
|
|
667
|
+
* calls in its open step receive error results, followed by `step/end` and a
|
|
668
|
+
* `turn/end` carrying the cause's reason. Calls in closed steps remain unchanged.
|
|
669
|
+
* Sequences continue the log and timestamps reuse the last real event. A balanced or empty log returns no
|
|
670
|
+
* events.
|
|
663
671
|
*
|
|
664
|
-
*
|
|
672
|
+
* Package-internal: each cause has exactly one owner, so external callers go
|
|
673
|
+
* through {@link interruptedTurnClosers} (persistence crash recovery) or
|
|
674
|
+
* `buildForkSeed` in `./fork.ts` (fork seeds) instead of selecting a cause.
|
|
675
|
+
*
|
|
676
|
+
* @param events - the log to scan: a valid committed prefix, possibly ending
|
|
677
|
+
* inside an open turn (a crash tail or a mid-turn fork cut).
|
|
678
|
+
* @param cause - why the turn is being closed; selects the `turn/end` reason
|
|
679
|
+
* and the model-visible wording of synthetic error tool results.
|
|
665
680
|
* @returns the synthetic closer events to append after `events`, in order; empty when the log is already balanced.
|
|
666
681
|
*/
|
|
667
|
-
function
|
|
682
|
+
function openTurnClosers(events, cause) {
|
|
668
683
|
let openTurn = null;
|
|
669
684
|
let openStep = null;
|
|
670
685
|
const pendingCalls = /* @__PURE__ */ new Map();
|
|
@@ -705,23 +720,21 @@ function interruptedTurnClosers(events) {
|
|
|
705
720
|
let seq = last.seq + 1;
|
|
706
721
|
const time = last.time;
|
|
707
722
|
const closers = [];
|
|
723
|
+
const text = CLOSER_TEXT[cause.kind];
|
|
708
724
|
for (const [callId, { step, callSeq }] of pendingCalls) {
|
|
709
725
|
const started = callSeq !== void 0;
|
|
710
726
|
const message = deepFreeze({
|
|
711
|
-
id: brandString(
|
|
712
|
-
role: "
|
|
727
|
+
id: brandString(`${cause.kind}-tool-result-${callId}-${seq}`),
|
|
728
|
+
role: "tool",
|
|
729
|
+
toolCallId: callId,
|
|
730
|
+
isError: true,
|
|
713
731
|
source: {
|
|
714
732
|
kind: "tool",
|
|
715
733
|
callId
|
|
716
734
|
},
|
|
717
735
|
content: [{
|
|
718
|
-
type: "
|
|
719
|
-
|
|
720
|
-
isError: true,
|
|
721
|
-
content: [{
|
|
722
|
-
type: "text",
|
|
723
|
-
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."
|
|
724
|
-
}]
|
|
736
|
+
type: "text",
|
|
737
|
+
text: started ? text.started : text.notStarted
|
|
725
738
|
}]
|
|
726
739
|
});
|
|
727
740
|
closers.push({
|
|
@@ -759,11 +772,87 @@ function interruptedTurnClosers(events) {
|
|
|
759
772
|
time,
|
|
760
773
|
data: {
|
|
761
774
|
turn: openTurn,
|
|
762
|
-
reason: { kind:
|
|
775
|
+
reason: { kind: cause.kind }
|
|
763
776
|
}
|
|
764
777
|
});
|
|
765
778
|
return closers;
|
|
766
779
|
}
|
|
780
|
+
/**
|
|
781
|
+
* Crash-recovery entry point: synthetic closers that balance a persisted log
|
|
782
|
+
* whose tail turn was interrupted. Used by crash-recovery callers; fork
|
|
783
|
+
* seeds receive their `forked`-cause closers through `buildForkSeed` in
|
|
784
|
+
* `./fork.ts`, and cause selection stays internal to those two owners.
|
|
785
|
+
*
|
|
786
|
+
* @param events - the persisted log to scan, possibly ending inside an open turn.
|
|
787
|
+
* @returns the synthetic `interrupted` closer events to append after `events`; empty when the log is already balanced.
|
|
788
|
+
*/
|
|
789
|
+
function interruptedTurnClosers(events) {
|
|
790
|
+
return openTurnClosers(events, { kind: "interrupted" });
|
|
791
|
+
}
|
|
792
|
+
//#endregion
|
|
793
|
+
//#region lib/types/fork.js
|
|
794
|
+
/**
|
|
795
|
+
* Fork seed construction over an exact source-event prefix.
|
|
796
|
+
* @module @deepseek-ai/dsh-session/fork
|
|
797
|
+
*/
|
|
798
|
+
/**
|
|
799
|
+
* Copy an inclusive event prefix, mark its inherited cut, and close its open tail with forked results
|
|
800
|
+
* and step/turn endings. Closed steps and turns are preserved unchanged.
|
|
801
|
+
* The caller validates that the boundary is an existing contiguous event seq;
|
|
802
|
+
* Session construction snapshots the borrowed events before publication.
|
|
803
|
+
*
|
|
804
|
+
* @param events - source log with contiguous seqs from zero.
|
|
805
|
+
* @param boundary - inclusive source event seq the child inherits through.
|
|
806
|
+
* @returns a new array retaining the source event objects, followed by synthetic
|
|
807
|
+
* closers outside the inherited prefix counted by `inheritedEventCount`.
|
|
808
|
+
*/
|
|
809
|
+
function buildForkSeed(events, boundary) {
|
|
810
|
+
const prefix = events.slice(0, boundary + 1);
|
|
811
|
+
prefix.push({
|
|
812
|
+
type: "session/end-seed",
|
|
813
|
+
seq: SessionSeq(boundary + 1),
|
|
814
|
+
time: events[boundary].time,
|
|
815
|
+
data: { inherited: true }
|
|
816
|
+
});
|
|
817
|
+
return prefix.concat(openTurnClosers(prefix, { kind: "forked" }));
|
|
818
|
+
}
|
|
819
|
+
//#endregion
|
|
820
|
+
//#region lib/types/preparation.js
|
|
821
|
+
/**
|
|
822
|
+
* Ownership of one unpublished Session before registry publication.
|
|
823
|
+
* @module @deepseek-ai/dsh-session/preparation
|
|
824
|
+
*/
|
|
825
|
+
/**
|
|
826
|
+
* One exact unpublished Session and the provider state that keeps it usable.
|
|
827
|
+
* Disposal is synchronous and idempotent. Providers decide whether release
|
|
828
|
+
* returns the Session to a cache or discards it; publication may consume that
|
|
829
|
+
* state before disposal, making the callback a no-op.
|
|
830
|
+
*/
|
|
831
|
+
var SessionPreparation = class SessionPreparation {
|
|
832
|
+
options;
|
|
833
|
+
released = false;
|
|
834
|
+
/** The exact Session to use for setup and publication. */
|
|
835
|
+
session;
|
|
836
|
+
constructor(session, options) {
|
|
837
|
+
this.options = options;
|
|
838
|
+
this.session = session;
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Wrap an unpublished Session in one preparation lifetime.
|
|
842
|
+
* @param session - exact unpublished Session.
|
|
843
|
+
* @param options - optional provider release behavior.
|
|
844
|
+
* @returns a preparation disposed after publication or rollback.
|
|
845
|
+
*/
|
|
846
|
+
static create(session, options) {
|
|
847
|
+
return new SessionPreparation(session, options ?? {});
|
|
848
|
+
}
|
|
849
|
+
/** Release provider state once when this preparation leaves its caller. */
|
|
850
|
+
[Symbol.dispose]() {
|
|
851
|
+
if (this.released) return;
|
|
852
|
+
this.released = true;
|
|
853
|
+
this.options.release?.();
|
|
854
|
+
}
|
|
855
|
+
};
|
|
767
856
|
//#endregion
|
|
768
857
|
//#region lib/types/seq-ranges.js
|
|
769
858
|
/** Lossless range encoding for JSONL `sourceEventSeqs` arrays. */
|
|
@@ -834,7 +923,7 @@ function validateSessionHeader(id, input) {
|
|
|
834
923
|
if (input === null || typeof input !== "object" || Array.isArray(input)) throw new Error("session header is not a plain JSON record");
|
|
835
924
|
const record = input;
|
|
836
925
|
if (Object.hasOwn(record, "seedLength")) throw new Error("session header has invalid field \"seedLength\"");
|
|
837
|
-
if (record.version !==
|
|
926
|
+
if (record.version !== 4) throw new Error(`session header version must be 4, got ${String(record.version)}`);
|
|
838
927
|
if (record.id !== id) throw new Error(`session header id "${String(record.id)}" does not match session id "${id}"`);
|
|
839
928
|
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");
|
|
840
929
|
if (record.cwd !== void 0) {
|
|
@@ -859,7 +948,7 @@ function validateRestoredSessionHeader(id, input) {
|
|
|
859
948
|
/** Detach, validate, and freeze the creation metadata published by a session. */
|
|
860
949
|
function snapshotSessionHeader(id, source) {
|
|
861
950
|
const snapshot = snapshotJsonValue(source === void 0 ? {
|
|
862
|
-
version:
|
|
951
|
+
version: 4,
|
|
863
952
|
id,
|
|
864
953
|
createdAt: Date.now(),
|
|
865
954
|
isSeeded: false
|
|
@@ -884,6 +973,7 @@ function adoptSessionEvent(event) {
|
|
|
884
973
|
case "user/message":
|
|
885
974
|
deepFreeze(event.data);
|
|
886
975
|
break;
|
|
976
|
+
case "developer/message":
|
|
887
977
|
case "system/message":
|
|
888
978
|
case "assistant/message":
|
|
889
979
|
case "tool/result":
|
|
@@ -922,6 +1012,7 @@ function assertSessionEventEnvelope(value, index) {
|
|
|
922
1012
|
validateSessionEventData(event, `seed ${type} at index ${index}`);
|
|
923
1013
|
switch (type) {
|
|
924
1014
|
case "request/header":
|
|
1015
|
+
case "developer/message":
|
|
925
1016
|
case "system/message":
|
|
926
1017
|
case "user/message":
|
|
927
1018
|
case "assistant/attempt":
|
|
@@ -970,15 +1061,16 @@ function assertAdapterDefaults(value, config, index) {
|
|
|
970
1061
|
const defaults = value;
|
|
971
1062
|
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`);
|
|
972
1063
|
}
|
|
973
|
-
/** The
|
|
1064
|
+
/** The surface event types whose payload carries an identified message. */
|
|
974
1065
|
function isMessageEventType(type) {
|
|
975
|
-
return type === "system/message" || type === "user/message" || type === "assistant/message" || type === "tool/result";
|
|
1066
|
+
return type === "developer/message" || type === "system/message" || type === "user/message" || type === "assistant/message" || type === "tool/result";
|
|
976
1067
|
}
|
|
977
1068
|
const MESSAGE_ROLE_BY_TYPE = {
|
|
978
1069
|
"system/message": "system",
|
|
1070
|
+
"developer/message": "developer",
|
|
979
1071
|
"user/message": "user",
|
|
980
1072
|
"assistant/message": "assistant",
|
|
981
|
-
"tool/result": "
|
|
1073
|
+
"tool/result": "tool"
|
|
982
1074
|
};
|
|
983
1075
|
/** Validate only the event-specific invariants needed to safely replay a message. */
|
|
984
1076
|
function assertMessageEventShape(event, subject) {
|
|
@@ -996,7 +1088,7 @@ function assertMessageEventShape(event, subject) {
|
|
|
996
1088
|
if (!Array.isArray(messageRecord["content"])) throw new Error(`${subject} message has invalid content`);
|
|
997
1089
|
const sourceRecord = source;
|
|
998
1090
|
if (type === "system/message") {
|
|
999
|
-
if (sourceRecord["kind"] !== "
|
|
1091
|
+
if (sourceRecord["kind"] !== "system-prompt") throw new Error(`${subject} message must have system-prompt source`);
|
|
1000
1092
|
return;
|
|
1001
1093
|
}
|
|
1002
1094
|
if (type === "assistant/message") {
|
|
@@ -1005,10 +1097,7 @@ function assertMessageEventShape(event, subject) {
|
|
|
1005
1097
|
}
|
|
1006
1098
|
if (type !== "tool/result") return;
|
|
1007
1099
|
if (sourceRecord["kind"] !== "tool" || typeof sourceRecord["callId"] !== "string" || sourceRecord["callId"] === "") throw new Error(`${subject} message must have tool source`);
|
|
1008
|
-
|
|
1009
|
-
const block = content[0];
|
|
1010
|
-
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`);
|
|
1011
|
-
if (block["toolCallId"] !== sourceRecord["callId"]) throw new Error(`${subject} message has mismatched tool call ids`);
|
|
1100
|
+
if (messageRecord["toolCallId"] !== sourceRecord["callId"]) throw new Error(`${subject} message has mismatched tool call ids`);
|
|
1012
1101
|
}
|
|
1013
1102
|
/** Whether an unknown value carries the current provider/model pair. */
|
|
1014
1103
|
function hasProviderModel(value) {
|
|
@@ -1065,30 +1154,26 @@ var Session = class Session {
|
|
|
1065
1154
|
return this.header.id;
|
|
1066
1155
|
}
|
|
1067
1156
|
/**
|
|
1068
|
-
* The
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
1072
|
-
* the constructor-input boundary for lifecycle ownership and persistence
|
|
1073
|
-
* adoption; consumers that need complete canonical history still start at
|
|
1074
|
-
* seq 0. Distinct from {@link inheritedEventCount}, the DURABLE
|
|
1075
|
-
* fork-lineage cut: a resumed session's constructor seed is its full stored
|
|
1076
|
-
* log, while the inherited count keeps the original fork value — this field is the
|
|
1077
|
-
* in-process construction fact.
|
|
1157
|
+
* The constructor seed length (0 without one), before any marker appended
|
|
1158
|
+
* during construction. Seed events never publish on `session/event`. A
|
|
1159
|
+
* marker appended before the store attaches occupies this seq without
|
|
1160
|
+
* publishing either; otherwise this seq is available for the next append.
|
|
1078
1161
|
*
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
1085
|
-
*
|
|
1086
|
-
* When this lifecycle appends the marker, it occupies this seq before the
|
|
1087
|
-
* store attaches and therefore does not publish either. Otherwise this seq
|
|
1088
|
-
* holds an ordinary published write.
|
|
1162
|
+
* This in-process offset is not persisted. A fork seed can already contain
|
|
1163
|
+
* the child's inherited marker and synthetic closers, so its child-owned
|
|
1164
|
+
* history starts at {@link inheritedEventCount}, before this offset. A
|
|
1165
|
+
* resumed Session's seed contains its full stored log, while its inherited
|
|
1166
|
+
* count keeps the durable fork cut. Consumers needing complete canonical
|
|
1167
|
+
* history start at seq 0.
|
|
1089
1168
|
*/
|
|
1090
1169
|
firstLiveSeq;
|
|
1091
1170
|
/**
|
|
1171
|
+
* First event produced for this object lifecycle. A new fork includes its
|
|
1172
|
+
* child-owned seed marker and closers; a restored Session starts after its
|
|
1173
|
+
* complete stored prefix. This in-process capture offset is not persisted.
|
|
1174
|
+
*/
|
|
1175
|
+
firstLifecycleSeq;
|
|
1176
|
+
/**
|
|
1092
1177
|
* Create a detached session by validating and snapshotting borrowed seed
|
|
1093
1178
|
* events and storage metadata.
|
|
1094
1179
|
* @param id - session identity.
|
|
@@ -1142,10 +1227,14 @@ var Session = class Session {
|
|
|
1142
1227
|
const inheritedEventCount = SessionLogOffset(suppliedInheritedEventCount ?? 0);
|
|
1143
1228
|
if (!this.header.isSeeded && inheritedEventCount !== 0) throw new Error("unseeded session inherited event count must be 0");
|
|
1144
1229
|
if (inheritedEventCount > this.log.length) throw new Error("session inherited event count exceeds its event log");
|
|
1145
|
-
|
|
1230
|
+
const seedMarker = this.log[inheritedEventCount];
|
|
1231
|
+
const markedSeed = seedMarker?.type === "session/end-seed" && seedMarker.data.inherited === true;
|
|
1232
|
+
if (mode === "snapshot" && this.header.isSeeded && inheritedEventCount !== this.log.length && !markedSeed) throw new Error("seeded session constructor seed must equal its inherited prefix or mark its inherited cut");
|
|
1233
|
+
if (markedSeed && this.log.slice(inheritedEventCount + 1).some((event) => event.type === "session/end-seed" && event.data.inherited === true)) throw new Error("session inherited event count must identify the final inherited marker");
|
|
1146
1234
|
this.inheritedEventCount = inheritedEventCount;
|
|
1147
|
-
|
|
1148
|
-
|
|
1235
|
+
this.firstLifecycleSeq = mode === "snapshot" && this.header.isSeeded ? inheritedEventCount : this.firstLiveSeq;
|
|
1236
|
+
if (seed !== void 0 && mode === "snapshot" && this.header.isSeeded && !markedSeed) this.append("session/end-seed", { inherited: true });
|
|
1237
|
+
else if (seed !== void 0 && !(mode === "snapshot" && this.header.isSeeded) && this.log.at(-1)?.type !== "session/end-seed") this.append("session/end-seed", {});
|
|
1149
1238
|
}
|
|
1150
1239
|
/** Cached immutable full snapshot of the private append-only log. */
|
|
1151
1240
|
eventsSnapshot;
|
|
@@ -1479,7 +1568,7 @@ var SessionStore = class extends Service {
|
|
|
1479
1568
|
const seed = options?.seed;
|
|
1480
1569
|
const meta = options?.meta;
|
|
1481
1570
|
const header = {
|
|
1482
|
-
version:
|
|
1571
|
+
version: 4,
|
|
1483
1572
|
id: sessionId,
|
|
1484
1573
|
createdAt: meta?.createdAt ?? Date.now(),
|
|
1485
1574
|
...meta?.cwd === void 0 ? {} : { cwd: meta.cwd },
|
|
@@ -1651,10 +1740,12 @@ var SessionStore = class extends Service {
|
|
|
1651
1740
|
return [...this.store.values()].map((entry) => entry.session);
|
|
1652
1741
|
}
|
|
1653
1742
|
/**
|
|
1654
|
-
* Create a live child session from
|
|
1743
|
+
* Create a live child session from an exact prefix of a live source.
|
|
1655
1744
|
* `boundary` is an inclusive source event seq; omitted means the source's
|
|
1656
|
-
* current last event.
|
|
1657
|
-
*
|
|
1745
|
+
* current last event. An open tail receives synthetic tool results and
|
|
1746
|
+
* step/turn closers with the forked cause. Closed steps and turns remain
|
|
1747
|
+
* unchanged, including any failed tool calls already missing results.
|
|
1748
|
+
* `inheritedEventCount` counts only copied source events, excluding these closers.
|
|
1658
1749
|
*
|
|
1659
1750
|
* @param source - Live source session object or id.
|
|
1660
1751
|
* @param boundary - Inclusive source event seq to fork through; omitted means
|
|
@@ -1667,10 +1758,12 @@ var SessionStore = class extends Service {
|
|
|
1667
1758
|
fork(source, boundary, childSessionId) {
|
|
1668
1759
|
if (childSessionId !== void 0 && this.get(childSessionId) !== void 0) throw new SessionForkError(`session "${childSessionId}" already exists`, "SESSION_ALREADY_EXISTS");
|
|
1669
1760
|
const liveSource = this._resolveForkSource(source);
|
|
1670
|
-
const
|
|
1761
|
+
const events = liveSource.snapshotEvents();
|
|
1762
|
+
const resolved = this._forkBoundary(liveSource.id, events, boundary);
|
|
1763
|
+
const seed = resolved === void 0 ? [] : buildForkSeed(events, resolved);
|
|
1671
1764
|
return this.create(childSessionId, {
|
|
1672
1765
|
seed,
|
|
1673
|
-
inheritedEventCount: SessionLogOffset(
|
|
1766
|
+
inheritedEventCount: SessionLogOffset(resolved === void 0 ? 0 : resolved + 1),
|
|
1674
1767
|
meta: {
|
|
1675
1768
|
...liveSource.header.cwd !== void 0 ? { cwd: liveSource.header.cwd } : {},
|
|
1676
1769
|
parentSession: liveSource.id,
|
|
@@ -1678,25 +1771,22 @@ var SessionStore = class extends Service {
|
|
|
1678
1771
|
}
|
|
1679
1772
|
});
|
|
1680
1773
|
}
|
|
1681
|
-
|
|
1682
|
-
const lastEvent =
|
|
1774
|
+
_forkBoundary(sessionId, events, requestedBoundary) {
|
|
1775
|
+
const lastEvent = events.at(-1);
|
|
1683
1776
|
let boundary;
|
|
1684
1777
|
if (requestedBoundary !== void 0) boundary = requestedBoundary;
|
|
1685
1778
|
else {
|
|
1686
|
-
if (lastEvent === void 0) return
|
|
1779
|
+
if (lastEvent === void 0) return void 0;
|
|
1687
1780
|
boundary = lastEvent.seq;
|
|
1688
1781
|
}
|
|
1689
|
-
if (!Number.isSafeInteger(boundary) || boundary < 0) throw new SessionForkError(`fork boundary for session "${
|
|
1690
|
-
if (boundary >=
|
|
1782
|
+
if (!Number.isSafeInteger(boundary) || boundary < 0) throw new SessionForkError(`fork boundary for session "${sessionId}" must be a non-negative safe integer, got ${String(boundary)}`, "INVALID_BOUNDARY");
|
|
1783
|
+
if (boundary >= events.length) {
|
|
1691
1784
|
const lastSeq = lastEvent?.seq;
|
|
1692
|
-
throw new SessionForkError(`fork boundary ${boundary} does not exist in session "${
|
|
1785
|
+
throw new SessionForkError(`fork boundary ${boundary} does not exist in session "${sessionId}" (last seq: ${lastSeq ?? "none"})`, "INVALID_BOUNDARY");
|
|
1693
1786
|
}
|
|
1694
|
-
const boundaryEvent =
|
|
1695
|
-
if (boundaryEvent === void 0 || boundaryEvent.seq !== boundary) throw new SessionForkError(`fork boundary ${boundary} does not match a contiguous event seq in session "${
|
|
1696
|
-
|
|
1697
|
-
const lastTurnBoundary = events.findLast((event) => event.type === "turn/start" || event.type === "turn/end");
|
|
1698
|
-
if (lastTurnBoundary?.type === "turn/start") throw new SessionForkError(`fork boundary ${boundary} in session "${session.id}" ends inside open turn ${lastTurnBoundary.data.turn}`, "OPEN_TURN");
|
|
1699
|
-
return events;
|
|
1787
|
+
const boundaryEvent = events[boundary];
|
|
1788
|
+
if (boundaryEvent === void 0 || boundaryEvent.seq !== boundary) throw new SessionForkError(`fork boundary ${boundary} does not match a contiguous event seq in session "${sessionId}"`, "INVALID_BOUNDARY");
|
|
1789
|
+
return boundary;
|
|
1700
1790
|
}
|
|
1701
1791
|
_resolveForkSource(source) {
|
|
1702
1792
|
if (typeof source === "string") {
|
|
@@ -1711,4 +1801,4 @@ var SessionStore = class extends Service {
|
|
|
1711
1801
|
}
|
|
1712
1802
|
};
|
|
1713
1803
|
//#endregion
|
|
1714
|
-
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 };
|
|
1804
|
+
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, buildForkSeed, canonicalHeader, decodeSeqRanges, deriveEventMessage, encodeSeqRanges, foldRequestHeader, foldSurface, headerEquals, interruptedTurnClosers, isAppendSurfaceEvent, isReplacementSurfaceEvent, isSurfaceEligibleType, isSurfaceEvent, snapshotSessionEvent };
|
package/lib/invariant.js
CHANGED
|
@@ -44,6 +44,9 @@ function validateEvent(trace, event, fail) {
|
|
|
44
44
|
if (event.data.step !== trace.nextStep) fail(`step/start expected step ${trace.nextStep} in turn ${event.data.turn}, got ${event.data.step}`);
|
|
45
45
|
openStep = event.data.step;
|
|
46
46
|
break;
|
|
47
|
+
case "developer/message":
|
|
48
|
+
requireOpenStep(trace, "developer/message", event.data.turn, event.data.step, fail);
|
|
49
|
+
break;
|
|
47
50
|
case "step/end":
|
|
48
51
|
requireOpenStep(trace, "step/end", event.data.turn, event.data.step, fail);
|
|
49
52
|
pendingCalls = { kind: "clear" };
|
|
@@ -70,7 +73,7 @@ function validateEvent(trace, event, fail) {
|
|
|
70
73
|
}
|
|
71
74
|
requireOpenStep(trace, "tool/result", event.data.turn, event.data.step, fail);
|
|
72
75
|
const callId = event.data.message.source.callId;
|
|
73
|
-
const syntheticNotStarted = event.data.message.
|
|
76
|
+
const syntheticNotStarted = event.data.message.isError === true && event.data.error?.code === "TOOL_NOT_STARTED";
|
|
74
77
|
if (!trace.pendingCalls.has(callId) && !syntheticNotStarted) fail(`tool/result for ${callId} with no prior tool/call in this step`);
|
|
75
78
|
pendingCalls = {
|
|
76
79
|
kind: "delete",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fork seed construction over an exact source-event prefix.
|
|
3
|
+
* @module @deepseek-ai/dsh-session/fork
|
|
4
|
+
*/
|
|
5
|
+
import type { SessionEvent, SessionSeq as SessionSeqType } from './types.ts';
|
|
6
|
+
/**
|
|
7
|
+
* Copy an inclusive event prefix, mark its inherited cut, and close its open tail with forked results
|
|
8
|
+
* and step/turn endings. Closed steps and turns are preserved unchanged.
|
|
9
|
+
* The caller validates that the boundary is an existing contiguous event seq;
|
|
10
|
+
* Session construction snapshots the borrowed events before publication.
|
|
11
|
+
*
|
|
12
|
+
* @param events - source log with contiguous seqs from zero.
|
|
13
|
+
* @param boundary - inclusive source event seq the child inherits through.
|
|
14
|
+
* @returns a new array retaining the source event objects, followed by synthetic
|
|
15
|
+
* closers outside the inherited prefix counted by `inheritedEventCount`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildForkSeed(events: readonly SessionEvent[], boundary: SessionSeqType): SessionEvent[];
|
|
18
|
+
//# sourceMappingURL=fork.d.ts.map
|