@deepseek-ai/dsh-session 0.1.2-rc.1 → 0.1.3-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 +6 -7
- package/README.zh.md +6 -7
- package/lib/index.js +73 -376
- package/lib/invariant.js +2 -2
- package/lib/types/index.d.ts +23 -20
- package/lib/types/index.js +73 -63
- package/lib/types/invariant.js +2 -2
- package/lib/types/known-event-types.js +3 -1
- package/lib/types/surface.d.ts +1 -1
- package/lib/types/surface.js +8 -5
- package/lib/types/types.d.ts +67 -56
- package/lib/types/types.js +9 -10
- package/package.json +10 -14
- package/lib/types/chunk-rows.d.ts +0 -106
- package/lib/types/chunk-rows.js +0 -328
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Service } from "@deepseek-ai/cordis";
|
|
2
2
|
import { isAbsolute } from "node:path";
|
|
3
3
|
import { brandNumber, brandString } from "@deepseek-ai/dsh-brand";
|
|
4
|
-
import { deepFreeze, snapshotJsonValue } from "@deepseek-ai/dsh-util-values";
|
|
4
|
+
import { assertNever, deepFreeze, snapshotJsonValue } from "@deepseek-ai/dsh-util-values";
|
|
5
5
|
import { scopeOf, scopeTarget } from "@deepseek-ai/dsh-scope";
|
|
6
6
|
import { callConfigEquals } from "@deepseek-ai/dsh-llm";
|
|
7
7
|
//#region lib/types/types.js
|
|
@@ -32,11 +32,11 @@ function SessionLogOffset(value) {
|
|
|
32
32
|
return brandNumber(value);
|
|
33
33
|
}
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
35
|
+
* Current logical Session format version, stamped into every newly written
|
|
36
|
+
* {@link SessionHeader}. Current Session and persistence code accept only this
|
|
37
|
+
* value; header-only readers classify supported historical formats, while an
|
|
38
|
+
* event-body read composes the build-static adjacent chain and publishes only
|
|
39
|
+
* this final generation before constructing a Session.
|
|
40
40
|
*
|
|
41
41
|
* The version is a single monotonic integer with no major/minor split. Whether
|
|
42
42
|
* a bump is needed is decided by what the WRITER emits, never by what a newer
|
|
@@ -49,12 +49,11 @@ function SessionLogOffset(value) {
|
|
|
49
49
|
* Adding an ordinary event type does not bump — the per-event
|
|
50
50
|
* {@link SessionEvent.ignorable} guard covers vocabulary growth instead. When
|
|
51
51
|
* in doubt, bump: a near-identity upgrade step is almost free, a missed bump
|
|
52
|
-
* makes older runtimes read new logs wrong silently. The
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* (`.agents/notes/implemented/architecture/2026-08-10-session-log-version-mechanism.md`).
|
|
52
|
+
* makes older runtimes read new logs wrong silently. The released migration,
|
|
53
|
+
* immutable prior-generation, and current fast-path rules are recorded in
|
|
54
|
+
* `.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md`.
|
|
56
55
|
*/
|
|
57
|
-
const SESSION_FORMAT_VERSION =
|
|
56
|
+
const SESSION_FORMAT_VERSION = 2;
|
|
58
57
|
//#endregion
|
|
59
58
|
//#region lib/types/surface.js
|
|
60
59
|
/**
|
|
@@ -115,7 +114,7 @@ function isReplacementSurfaceEvent(event) {
|
|
|
115
114
|
}
|
|
116
115
|
/**
|
|
117
116
|
* Project a single event into the LLM message it derives to, or null when it
|
|
118
|
-
* produces none — a non-surface event (
|
|
117
|
+
* produces none — a non-surface event (attempt, boundary, log-only record) or an
|
|
119
118
|
* empty-content assistant/message (which exists only to host usage). This is
|
|
120
119
|
* THE per-node projection rule: `Session.deriveMessages` folds it over the
|
|
121
120
|
* live surface, external reconstructors and pure projections fold the same
|
|
@@ -170,10 +169,11 @@ function surfaceOpOf(event) {
|
|
|
170
169
|
/** Validate cited source-event seqs against prior log entries and the replacement range. */
|
|
171
170
|
function assertProvenance(event, shadowedSeqs) {
|
|
172
171
|
const raw = event.sourceEventSeqs;
|
|
172
|
+
if (event.type === "assistant/message" && raw !== void 0) throw new Error("assistant/message embeds its source stream and cannot carry sourceEventSeqs");
|
|
173
173
|
const sources = /* @__PURE__ */ new Set();
|
|
174
174
|
if (raw !== void 0) {
|
|
175
175
|
if (!Array.isArray(raw)) throw new Error(`sourceEventSeqs on event at seq ${event.seq} must be an array when present`);
|
|
176
|
-
if (raw.length === 0
|
|
176
|
+
if (raw.length === 0) throw new Error("sourceEventSeqs must not be empty");
|
|
177
177
|
let nonEarlierSource;
|
|
178
178
|
for (const source of raw) {
|
|
179
179
|
if (!isEventSeq(source)) throw new Error(`session event "${event.type}" sourceEventSeqs must densely contain non-negative safe integers`);
|
|
@@ -575,321 +575,6 @@ function interruptedTurnClosers(events) {
|
|
|
575
575
|
return closers;
|
|
576
576
|
}
|
|
577
577
|
//#endregion
|
|
578
|
-
//#region lib/types/chunk-rows.js
|
|
579
|
-
/**
|
|
580
|
-
* Lossless row packing for `assistant/chunk` delta runs. Providers stream
|
|
581
|
-
* token-sized deltas, so a log stores hundreds of near-identical event lines
|
|
582
|
-
* whose JSON envelopes dwarf their payloads (~56× measured on a real DeepSeek
|
|
583
|
-
* session). This module packs each run of consecutive same-block delta chunks
|
|
584
|
-
* into ONE storage row — `text-chunks`, `reasoning-chunks`, or
|
|
585
|
-
* `tool-call-chunks` — and expands rows back to the exact original events.
|
|
586
|
-
*
|
|
587
|
-
* Packed rows are an encoding vocabulary, NOT session events: they never enter
|
|
588
|
-
* `Session.snapshotEvents()`, have no `SessionEventMap` entry, and use bare (slash-less)
|
|
589
|
-
* type tags so a reader cannot confuse them with the event taxonomy
|
|
590
|
-
* (precedent: the JSONL header line's `session` tag). Persistence and bounded
|
|
591
|
-
* history transport both use the codec. The encoder whitelists exact shapes —
|
|
592
|
-
* anything it does not fully recognize stays verbatim, so unknown fields or
|
|
593
|
-
* future chunk variants lose compression, never data. The decoder validates
|
|
594
|
-
* before expanding and fails loud on a malformed row-tagged value instead of
|
|
595
|
-
* silently dropping a whole run.
|
|
596
|
-
*
|
|
597
|
-
* @module @deepseek-ai/dsh-session/chunk-rows
|
|
598
|
-
*/
|
|
599
|
-
/**
|
|
600
|
-
* Minimum members before a run packs. Below it a row's envelope rivals the
|
|
601
|
-
* event lines it replaces. A format constant, not a tunable: both layouts
|
|
602
|
-
* decode identically, so changing it never invalidates stored logs.
|
|
603
|
-
*/
|
|
604
|
-
const MIN_RUN = 3;
|
|
605
|
-
function isRecord(value) {
|
|
606
|
-
return typeof value === "object" && value !== null;
|
|
607
|
-
}
|
|
608
|
-
/** Exact-key check: `value` has every key in `keys` and nothing else. */
|
|
609
|
-
function hasExactKeys(value, keys) {
|
|
610
|
-
return Object.keys(value).length === keys.length && keys.every((k) => Object.hasOwn(value, k));
|
|
611
|
-
}
|
|
612
|
-
/**
|
|
613
|
-
* Classify an event for packing: its delta kind when the ENTIRE shape
|
|
614
|
-
* (envelope, data, chunk — exact keys, primitive types, integer seq/time) is
|
|
615
|
-
* whitelisted, else `undefined` (store verbatim). Inputs come from live typed
|
|
616
|
-
* appends AND parsed fixture files, so the checks are structural, not
|
|
617
|
-
* type-trusted. Integer times keep gap encoding exact: a fractional time would
|
|
618
|
-
* reconstruct through float subtraction/addition, which need not round-trip.
|
|
619
|
-
*/
|
|
620
|
-
function classify(event) {
|
|
621
|
-
if (event.type !== "assistant/chunk") return void 0;
|
|
622
|
-
if (!hasExactKeys(event, [
|
|
623
|
-
"type",
|
|
624
|
-
"seq",
|
|
625
|
-
"time",
|
|
626
|
-
"data"
|
|
627
|
-
])) return void 0;
|
|
628
|
-
if (!Number.isSafeInteger(event.seq) || event.seq < 0 || Object.is(event.seq, -0) || !Number.isSafeInteger(event.time)) return void 0;
|
|
629
|
-
const data = event.data;
|
|
630
|
-
if (!isRecord(data) || !hasExactKeys(data, [
|
|
631
|
-
"turn",
|
|
632
|
-
"step",
|
|
633
|
-
"chunk"
|
|
634
|
-
])) return void 0;
|
|
635
|
-
if (typeof data.turn !== "number" || typeof data.step !== "number") return void 0;
|
|
636
|
-
const chunk = data.chunk;
|
|
637
|
-
if (!isRecord(chunk) || typeof chunk.index !== "number") return void 0;
|
|
638
|
-
switch (chunk.type) {
|
|
639
|
-
case "text-delta":
|
|
640
|
-
case "reasoning-delta": return hasExactKeys(chunk, [
|
|
641
|
-
"type",
|
|
642
|
-
"index",
|
|
643
|
-
"text"
|
|
644
|
-
]) && typeof chunk.text === "string" ? chunk.type : void 0;
|
|
645
|
-
case "tool-call-delta": return (hasExactKeys(chunk, [
|
|
646
|
-
"type",
|
|
647
|
-
"index",
|
|
648
|
-
"id",
|
|
649
|
-
"argumentsDelta"
|
|
650
|
-
]) || hasExactKeys(chunk, [
|
|
651
|
-
"type",
|
|
652
|
-
"index",
|
|
653
|
-
"id",
|
|
654
|
-
"name",
|
|
655
|
-
"argumentsDelta"
|
|
656
|
-
]) && typeof chunk.name === "string") && typeof chunk.id === "string" && typeof chunk.argumentsDelta === "string" ? chunk.type : void 0;
|
|
657
|
-
default: return;
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
/** The tool-call fields of a whitelisted delta chunk (only after {@link classify} returned `'tool-call-delta'`). */
|
|
661
|
-
function toolCallOf(event) {
|
|
662
|
-
return event.data.chunk;
|
|
663
|
-
}
|
|
664
|
-
/** The block index of a whitelisted delta chunk (not every {@link StreamChunk} variant carries one). */
|
|
665
|
-
function indexOf(event) {
|
|
666
|
-
return event.data.chunk.index;
|
|
667
|
-
}
|
|
668
|
-
/** Whether `next` extends a run ending in `prev` (same kind already checked by the caller). */
|
|
669
|
-
function continues(prev, next, kind) {
|
|
670
|
-
if (next.seq !== prev.seq + 1) return false;
|
|
671
|
-
if (!Number.isSafeInteger(next.time - prev.time)) return false;
|
|
672
|
-
if (next.data.turn !== prev.data.turn || next.data.step !== prev.data.step) return false;
|
|
673
|
-
if (indexOf(next) !== indexOf(prev)) return false;
|
|
674
|
-
if (kind !== "tool-call-delta") return true;
|
|
675
|
-
const a = toolCallOf(prev);
|
|
676
|
-
const b = toolCallOf(next);
|
|
677
|
-
return a.id === b.id && Object.hasOwn(a, "name") === Object.hasOwn(b, "name") && a.name === b.name;
|
|
678
|
-
}
|
|
679
|
-
/** Build the row for a completed run (`run.length >= MIN_RUN`, uniform per {@link continues}). */
|
|
680
|
-
function buildRow(kind, run) {
|
|
681
|
-
const first = run[0];
|
|
682
|
-
const base = {
|
|
683
|
-
turn: first.data.turn,
|
|
684
|
-
step: first.data.step,
|
|
685
|
-
index: indexOf(first),
|
|
686
|
-
dt: run.slice(1).map((event, i) => event.time - run[i].time)
|
|
687
|
-
};
|
|
688
|
-
const envelope = {
|
|
689
|
-
seq0: first.seq,
|
|
690
|
-
time0: first.time
|
|
691
|
-
};
|
|
692
|
-
if (kind === "tool-call-delta") {
|
|
693
|
-
const call = toolCallOf(first);
|
|
694
|
-
return {
|
|
695
|
-
type: "tool-call-chunks",
|
|
696
|
-
...envelope,
|
|
697
|
-
data: {
|
|
698
|
-
...base,
|
|
699
|
-
id: brandString(call.id),
|
|
700
|
-
...Object.hasOwn(call, "name") ? { name: call.name } : {},
|
|
701
|
-
args: run.map((event) => event.data.chunk.argumentsDelta)
|
|
702
|
-
}
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
|
-
const data = {
|
|
706
|
-
...base,
|
|
707
|
-
texts: run.map((event) => event.data.chunk.text)
|
|
708
|
-
};
|
|
709
|
-
return kind === "text-delta" ? {
|
|
710
|
-
type: "text-chunks",
|
|
711
|
-
...envelope,
|
|
712
|
-
data
|
|
713
|
-
} : {
|
|
714
|
-
type: "reasoning-chunks",
|
|
715
|
-
...envelope,
|
|
716
|
-
data
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
/**
|
|
720
|
-
* Pack an event batch for storage: each run of at least {@link MIN_RUN}
|
|
721
|
-
* consecutive whitelisted same-kind, same-block delta chunk events becomes one
|
|
722
|
-
* {@link ChunkRow}; every other event passes through verbatim, in order.
|
|
723
|
-
* Pure and stateless — safe over any array, including a batch whose runs were
|
|
724
|
-
* split by flush boundaries (the split runs simply pack per batch).
|
|
725
|
-
*
|
|
726
|
-
* @param events - the batch to encode, in log order.
|
|
727
|
-
* @returns the storage records to write, one JSONL line each.
|
|
728
|
-
*/
|
|
729
|
-
function packChunkRuns(events) {
|
|
730
|
-
const out = [];
|
|
731
|
-
let kind;
|
|
732
|
-
let run = [];
|
|
733
|
-
const flush = () => {
|
|
734
|
-
if (kind !== void 0 && run.length >= MIN_RUN) out.push(buildRow(kind, run));
|
|
735
|
-
else out.push(...run);
|
|
736
|
-
kind = void 0;
|
|
737
|
-
run = [];
|
|
738
|
-
};
|
|
739
|
-
for (const event of events) {
|
|
740
|
-
const k = classify(event);
|
|
741
|
-
if (k === void 0) {
|
|
742
|
-
flush();
|
|
743
|
-
out.push(event);
|
|
744
|
-
continue;
|
|
745
|
-
}
|
|
746
|
-
const delta = event;
|
|
747
|
-
const last = run[run.length - 1];
|
|
748
|
-
if (k === kind && last !== void 0 && continues(last, delta, k)) {
|
|
749
|
-
run.push(delta);
|
|
750
|
-
continue;
|
|
751
|
-
}
|
|
752
|
-
flush();
|
|
753
|
-
kind = k;
|
|
754
|
-
run = [delta];
|
|
755
|
-
}
|
|
756
|
-
flush();
|
|
757
|
-
return out;
|
|
758
|
-
}
|
|
759
|
-
/** Throw the uniform malformed-row diagnostic. */
|
|
760
|
-
function malformed(tag, why) {
|
|
761
|
-
throw new Error(`malformed ${tag} storage row: ${why}`);
|
|
762
|
-
}
|
|
763
|
-
/** Validate the shared run-data fields and the payload/dt arity; returns the member payload. */
|
|
764
|
-
function validateRunData(tag, data, payloadKey) {
|
|
765
|
-
if (typeof data.turn !== "number" || typeof data.step !== "number" || typeof data.index !== "number") malformed(tag, "turn/step/index must be numbers");
|
|
766
|
-
const payload = data[payloadKey];
|
|
767
|
-
if (!Array.isArray(payload) || payload.length === 0 || payload.some((entry) => typeof entry !== "string")) malformed(tag, `${payloadKey} must be a non-empty string array`);
|
|
768
|
-
const dt = data.dt;
|
|
769
|
-
if (!Array.isArray(dt) || dt.some((gap) => !Number.isSafeInteger(gap))) malformed(tag, "dt must be an array of safe integers");
|
|
770
|
-
if (dt.length !== payload.length - 1) malformed(tag, `dt length ${dt.length} does not match ${payload.length} members`);
|
|
771
|
-
return payload;
|
|
772
|
-
}
|
|
773
|
-
/** Validate a row-tagged parsed value's envelope and data, throwing on any malformation. */
|
|
774
|
-
function validateRow(value, tag) {
|
|
775
|
-
if (!hasExactKeys(value, [
|
|
776
|
-
"type",
|
|
777
|
-
"seq0",
|
|
778
|
-
"time0",
|
|
779
|
-
"data"
|
|
780
|
-
])) malformed(tag, "envelope must be exactly {type, seq0, time0, data}");
|
|
781
|
-
if (!Number.isSafeInteger(value.seq0) || value.seq0 < 0 || Object.is(value.seq0, -0)) malformed(tag, "seq0 must be a non-negative safe integer");
|
|
782
|
-
if (!Number.isSafeInteger(value.time0)) malformed(tag, "time0 must be a safe integer");
|
|
783
|
-
const data = value.data;
|
|
784
|
-
if (!isRecord(data)) malformed(tag, "data must be an object");
|
|
785
|
-
let payload;
|
|
786
|
-
if (tag === "tool-call-chunks") {
|
|
787
|
-
const withName = hasExactKeys(data, [
|
|
788
|
-
"turn",
|
|
789
|
-
"step",
|
|
790
|
-
"index",
|
|
791
|
-
"id",
|
|
792
|
-
"name",
|
|
793
|
-
"dt",
|
|
794
|
-
"args"
|
|
795
|
-
]);
|
|
796
|
-
if (!withName && !hasExactKeys(data, [
|
|
797
|
-
"turn",
|
|
798
|
-
"step",
|
|
799
|
-
"index",
|
|
800
|
-
"id",
|
|
801
|
-
"dt",
|
|
802
|
-
"args"
|
|
803
|
-
])) malformed(tag, "data must be exactly {turn, step, index, id, name?, dt, args}");
|
|
804
|
-
if (typeof data.id !== "string" || withName && typeof data.name !== "string") malformed(tag, "id (and name when present) must be strings");
|
|
805
|
-
payload = validateRunData(tag, data, "args");
|
|
806
|
-
} else {
|
|
807
|
-
if (!hasExactKeys(data, [
|
|
808
|
-
"turn",
|
|
809
|
-
"step",
|
|
810
|
-
"index",
|
|
811
|
-
"dt",
|
|
812
|
-
"texts"
|
|
813
|
-
])) malformed(tag, "data must be exactly {turn, step, index, dt, texts}");
|
|
814
|
-
payload = validateRunData(tag, data, "texts");
|
|
815
|
-
}
|
|
816
|
-
if (payload.length - 1 > Number.MAX_SAFE_INTEGER - value.seq0) malformed(tag, "member seqs must stay safe integers");
|
|
817
|
-
let time = value.time0;
|
|
818
|
-
for (const gap of data.dt) {
|
|
819
|
-
time += gap;
|
|
820
|
-
if (!Number.isSafeInteger(time)) malformed(tag, "member times must stay safe integers");
|
|
821
|
-
}
|
|
822
|
-
SessionSeq(value.seq0);
|
|
823
|
-
return value;
|
|
824
|
-
}
|
|
825
|
-
/** Expand a validated row back into its exact original events, in order. */
|
|
826
|
-
function expandRow(row) {
|
|
827
|
-
const members = row.type === "tool-call-chunks" ? row.data.args : row.data.texts;
|
|
828
|
-
const events = [];
|
|
829
|
-
let time = row.time0;
|
|
830
|
-
for (let k = 0; k < members.length; k++) {
|
|
831
|
-
if (k > 0) time += row.data.dt[k - 1];
|
|
832
|
-
let chunk;
|
|
833
|
-
switch (row.type) {
|
|
834
|
-
case "text-chunks":
|
|
835
|
-
chunk = {
|
|
836
|
-
type: "text-delta",
|
|
837
|
-
index: row.data.index,
|
|
838
|
-
text: members[k]
|
|
839
|
-
};
|
|
840
|
-
break;
|
|
841
|
-
case "reasoning-chunks":
|
|
842
|
-
chunk = {
|
|
843
|
-
type: "reasoning-delta",
|
|
844
|
-
index: row.data.index,
|
|
845
|
-
text: members[k]
|
|
846
|
-
};
|
|
847
|
-
break;
|
|
848
|
-
case "tool-call-chunks":
|
|
849
|
-
chunk = {
|
|
850
|
-
type: "tool-call-delta",
|
|
851
|
-
index: row.data.index,
|
|
852
|
-
id: row.data.id,
|
|
853
|
-
...Object.hasOwn(row.data, "name") ? { name: row.data.name } : {},
|
|
854
|
-
argumentsDelta: members[k]
|
|
855
|
-
};
|
|
856
|
-
break;
|
|
857
|
-
/* v8 ignore next 4 -- validateRow only returns the three row tags */
|
|
858
|
-
default: throw new Error(`chunk-rows received unsupported row ${String(row)}`);
|
|
859
|
-
}
|
|
860
|
-
events.push({
|
|
861
|
-
type: "assistant/chunk",
|
|
862
|
-
seq: SessionSeq(row.seq0 + k),
|
|
863
|
-
time,
|
|
864
|
-
data: {
|
|
865
|
-
turn: row.data.turn,
|
|
866
|
-
step: row.data.step,
|
|
867
|
-
chunk
|
|
868
|
-
}
|
|
869
|
-
});
|
|
870
|
-
}
|
|
871
|
-
return events;
|
|
872
|
-
}
|
|
873
|
-
/**
|
|
874
|
-
* Decode one parsed JSONL line value into the session event(s) it stores.
|
|
875
|
-
* Chunk-row-tagged values validate and expand (a malformed row throws — it is
|
|
876
|
-
* corrupt storage, and treating it as an event would silently drop a whole
|
|
877
|
-
* run); every other value passes through as a single event after admitting a
|
|
878
|
-
* numeric `seq` through the Session-sequence constructor.
|
|
879
|
-
*
|
|
880
|
-
* @param value - one line's `JSON.parse` result.
|
|
881
|
-
* @returns the stored events, in log order.
|
|
882
|
-
*/
|
|
883
|
-
function decodeStorageRecord(value) {
|
|
884
|
-
if (!isRecord(value)) return [value];
|
|
885
|
-
const tag = value.type;
|
|
886
|
-
if (tag !== "text-chunks" && tag !== "reasoning-chunks" && tag !== "tool-call-chunks") {
|
|
887
|
-
if (typeof value.seq === "number") SessionSeq(value.seq);
|
|
888
|
-
return [value];
|
|
889
|
-
}
|
|
890
|
-
return expandRow(validateRow(value, tag));
|
|
891
|
-
}
|
|
892
|
-
//#endregion
|
|
893
578
|
//#region lib/types/known-event-types.js
|
|
894
579
|
/**
|
|
895
580
|
* GENERATED by `scripts/gen-persistence-catalog.ts` — do not edit by hand; run
|
|
@@ -917,7 +602,7 @@ const KNOWN_SESSION_EVENT_TYPES = new Set([
|
|
|
917
602
|
"approval/asked",
|
|
918
603
|
"approval/decided",
|
|
919
604
|
"approval/policy",
|
|
920
|
-
"assistant/
|
|
605
|
+
"assistant/attempt",
|
|
921
606
|
"assistant/message",
|
|
922
607
|
"command/done",
|
|
923
608
|
"command/run",
|
|
@@ -925,6 +610,8 @@ const KNOWN_SESSION_EVENT_TYPES = new Set([
|
|
|
925
610
|
"compaction/prune",
|
|
926
611
|
"compaction/start",
|
|
927
612
|
"compaction/summary",
|
|
613
|
+
"feedback/message-delete",
|
|
614
|
+
"feedback/message-put",
|
|
928
615
|
"feedback/record",
|
|
929
616
|
"goal/change",
|
|
930
617
|
"hook/invoked",
|
|
@@ -1034,7 +721,7 @@ function validateSessionHeader(id, input) {
|
|
|
1034
721
|
if (input === null || typeof input !== "object" || Array.isArray(input)) throw new Error("session header is not a plain JSON record");
|
|
1035
722
|
const record = input;
|
|
1036
723
|
if (Object.hasOwn(record, "seedLength")) throw new Error("session header has invalid field \"seedLength\"");
|
|
1037
|
-
if (record.version !==
|
|
724
|
+
if (record.version !== 2) throw new Error(`session header version must be 2, got ${String(record.version)}`);
|
|
1038
725
|
if (record.id !== id) throw new Error(`session header id "${String(record.id)}" does not match session id "${id}"`);
|
|
1039
726
|
if (typeof record.createdAt !== "number" || !Number.isSafeInteger(record.createdAt) || record.createdAt < 0) throw new Error("session header createdAt must be a non-negative safe integer");
|
|
1040
727
|
if (record.cwd !== void 0) {
|
|
@@ -1059,7 +746,7 @@ function validateRestoredSessionHeader(id, input) {
|
|
|
1059
746
|
/** Detach, validate, and freeze the creation metadata published by a session. */
|
|
1060
747
|
function snapshotSessionHeader(id, source) {
|
|
1061
748
|
const snapshot = snapshotJsonValue(source === void 0 ? {
|
|
1062
|
-
version:
|
|
749
|
+
version: 2,
|
|
1063
750
|
id,
|
|
1064
751
|
createdAt: Date.now(),
|
|
1065
752
|
isSeeded: false
|
|
@@ -1097,23 +784,9 @@ function adoptSessionEvent(event) {
|
|
|
1097
784
|
function snapshotSessionEvent(event) {
|
|
1098
785
|
return adoptSessionEvent(structuredClone(event));
|
|
1099
786
|
}
|
|
1100
|
-
/** Deep-freeze one acyclic JSON tree without consuming the JavaScript call stack. */
|
|
1101
|
-
function freezeRestoredObject(value) {
|
|
1102
|
-
const pending = [value];
|
|
1103
|
-
while (pending.length > 0) {
|
|
1104
|
-
const current = pending.pop();
|
|
1105
|
-
Object.freeze(current);
|
|
1106
|
-
for (const key in current) {
|
|
1107
|
-
const child = current[key];
|
|
1108
|
-
if (child !== null && typeof child === "object") pending.push(child);
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
return value;
|
|
1112
|
-
}
|
|
1113
787
|
/** Validate the fixed event envelope after one-pass JSON materialization. */
|
|
1114
788
|
function assertSessionEventEnvelope(value, index) {
|
|
1115
789
|
const event = value;
|
|
1116
|
-
if (event["type"] === "request/header-delta") throw new Error(`seed event at index ${index} uses unsupported legacy request/header-delta format`);
|
|
1117
790
|
for (const key in event) switch (key) {
|
|
1118
791
|
case "type":
|
|
1119
792
|
case "seq":
|
|
@@ -1131,6 +804,7 @@ function assertSessionEventEnvelope(value, index) {
|
|
|
1131
804
|
switch (type) {
|
|
1132
805
|
case "request/header":
|
|
1133
806
|
case "user/message":
|
|
807
|
+
case "assistant/attempt":
|
|
1134
808
|
case "assistant/message":
|
|
1135
809
|
case "tool/result":
|
|
1136
810
|
assertCurrentLlmShape(event, index);
|
|
@@ -1150,10 +824,24 @@ function assertCurrentLlmShape(event, index) {
|
|
|
1150
824
|
const reasoningEffort = configRecord["reasoningEffort"];
|
|
1151
825
|
if (reasoningEffort !== void 0 && (typeof reasoningEffort !== "string" || reasoningEffort.length === 0)) throw new Error(`seed request/header at index ${index} has an invalid reasoningEffort`);
|
|
1152
826
|
assertAdapterDefaults(headerRecord?.["adapterDefaults"], configRecord, index);
|
|
827
|
+
const reason = record?.["reason"];
|
|
828
|
+
if (reason !== "initial" && reason !== "resume" && reason !== "change" && reason !== "series") throw new Error(`seed request/header at index ${index} has an invalid reason`);
|
|
829
|
+
if (record?.["startsSeries"] !== void 0 && record["startsSeries"] !== true) throw new Error(`seed request/header at index ${index} has an invalid startsSeries marker`);
|
|
1153
830
|
}
|
|
1154
831
|
const type = event["type"];
|
|
832
|
+
if (type === "assistant/attempt") {
|
|
833
|
+
assertAssistantSettlementShape(record, type, index);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
1155
836
|
if (type !== "user/message" && type !== "assistant/message" && type !== "tool/result") return;
|
|
1156
837
|
assertMessageEventShape(event, `seed ${type} at index ${index}`);
|
|
838
|
+
if (type === "assistant/message") assertAssistantSettlementShape(record, type, index);
|
|
839
|
+
}
|
|
840
|
+
/** Validate fields used directly by restored Session lifecycle logic without replaying the embedded stream. */
|
|
841
|
+
function assertAssistantSettlementShape(data, type, index) {
|
|
842
|
+
const turn = data?.["turn"];
|
|
843
|
+
const step = data?.["step"];
|
|
844
|
+
if (typeof turn !== "number" || !Number.isSafeInteger(turn) || turn < 0 || Object.is(turn, -0) || typeof step !== "number" || !Number.isSafeInteger(step) || step < 0 || Object.is(step, -0) || !Array.isArray(data?.["stream"])) throw new Error(`seed ${type} at index ${index} has invalid settlement fields`);
|
|
1157
845
|
}
|
|
1158
846
|
const allowedAdapterKeys = new Set(["reasoningEffort", "maxTokens"]);
|
|
1159
847
|
/** Validate adapter-default markers imported from a durable request header. */
|
|
@@ -1195,11 +883,6 @@ function hasProviderModel(value) {
|
|
|
1195
883
|
const pair = value;
|
|
1196
884
|
return typeof pair["provider"] === "string" && pair["provider"].length > 0 && typeof pair["model"] === "string" && pair["model"].length > 0;
|
|
1197
885
|
}
|
|
1198
|
-
/** Reject request-header vocabulary removed with the legacy delta codec. */
|
|
1199
|
-
function assertSupportedRequestHeader(type, data, location) {
|
|
1200
|
-
if (type === "request/header-delta") throw new Error(`${location} uses unsupported legacy request/header-delta format`);
|
|
1201
|
-
if (type === "request/header" && data !== null && typeof data === "object" && !Array.isArray(data) && data["reason"] === "fallback") throw new Error(`${location} uses unsupported legacy request/header reason "fallback"`);
|
|
1202
|
-
}
|
|
1203
886
|
/** Resolve one listener snapshot, including Cordis's internal dispatch checks. */
|
|
1204
887
|
function collectSessionCallbacks(ctx, args) {
|
|
1205
888
|
return [...ctx.events.dispatch("emit", args)];
|
|
@@ -1252,9 +935,10 @@ var Session = class Session {
|
|
|
1252
935
|
* The first seq appended IN THIS PROCESS: the length of the constructor
|
|
1253
936
|
* seed (0 without one). Events with smaller seq values entered through
|
|
1254
937
|
* construction — replay, fork, or resume — and were never published on the
|
|
1255
|
-
* `session/event` firehose (constructor seeds do not emit)
|
|
1256
|
-
*
|
|
1257
|
-
*
|
|
938
|
+
* `session/event` firehose (constructor seeds do not emit). This offset marks
|
|
939
|
+
* the constructor-input boundary for lifecycle ownership and persistence
|
|
940
|
+
* adoption; consumers that need complete canonical history still start at
|
|
941
|
+
* seq 0. Distinct from {@link inheritedEventCount}, the DURABLE
|
|
1258
942
|
* fork-lineage cut: a resumed session's constructor seed is its full stored
|
|
1259
943
|
* log, while the inherited count keeps the original fork value — this field is the
|
|
1260
944
|
* in-process construction fact.
|
|
@@ -1284,32 +968,34 @@ var Session = class Session {
|
|
|
1284
968
|
return new Session(id, seed, header, "snapshot", inheritedEventCount);
|
|
1285
969
|
}
|
|
1286
970
|
/**
|
|
1287
|
-
* Restore a detached session by
|
|
1288
|
-
*
|
|
1289
|
-
* and header fields are validated
|
|
971
|
+
* Restore a detached session by adopting an independently owned or deeply frozen seed.
|
|
972
|
+
* Runtime-required event fields, event envelopes, sequence continuity, surface
|
|
973
|
+
* transitions, and header fields are validated without copying or freezing events.
|
|
974
|
+
* Embedded Assistant streams remain opaque until a stream consumer or storage
|
|
975
|
+
* verifier reads them.
|
|
1290
976
|
* @param id - restored session identity.
|
|
1291
|
-
* @param seed -
|
|
1292
|
-
* @param header -
|
|
977
|
+
* @param seed - independently owned or deeply frozen events.
|
|
978
|
+
* @param header - independently owned storage metadata.
|
|
1293
979
|
* @param inheritedEventCount - exact fork-inherited prefix length decoded from storage.
|
|
980
|
+
* @param eventState - aliasing state carried from the operation that produced the seed.
|
|
1294
981
|
* @returns a restored detached session.
|
|
1295
982
|
*/
|
|
1296
|
-
static fromRestore(id, seed, header, inheritedEventCount) {
|
|
1297
|
-
return new Session(id, seed, header,
|
|
983
|
+
static fromRestore(id, seed, header, inheritedEventCount, eventState) {
|
|
984
|
+
return new Session(id, seed, header, eventState, inheritedEventCount);
|
|
1298
985
|
}
|
|
1299
986
|
constructor(id, seed, header, mode = "snapshot", suppliedInheritedEventCount) {
|
|
1300
|
-
const restoredHeader = mode === "
|
|
987
|
+
const restoredHeader = mode === "snapshot" ? void 0 : validateRestoredSessionHeader(id, header);
|
|
1301
988
|
if (seed !== void 0) for (const [index, source] of seed.entries()) {
|
|
1302
|
-
const snapshot = mode === "
|
|
989
|
+
const snapshot = mode === "snapshot" ? snapshotJsonValue(source) : source;
|
|
1303
990
|
if (snapshot === void 0) throw new Error(`seed event at index ${index} is not losslessly JSON-serializable`);
|
|
1304
991
|
assertSessionEventEnvelope(snapshot, index);
|
|
1305
|
-
assertSupportedRequestHeader(snapshot.type, snapshot.data, `seed event at index ${index}`);
|
|
1306
992
|
if (snapshot.seq !== index) throw new Error(`seed event at index ${index} has seq ${snapshot.seq} (expected ${index}); seed must be contiguous from 0`);
|
|
1307
993
|
try {
|
|
1308
994
|
this.surfaceManager.validateNext(snapshot);
|
|
1309
995
|
} catch (error) {
|
|
1310
996
|
throw new Error(`invalid seed event at index ${index}: ${error instanceof Error ? error.message : "invalid surface metadata"}`);
|
|
1311
997
|
}
|
|
1312
|
-
this.log.push(mode === "
|
|
998
|
+
this.log.push(mode === "snapshot" ? deepFreeze(snapshot) : snapshot);
|
|
1313
999
|
}
|
|
1314
1000
|
this.firstLiveSeq = SessionLogOffset(this.log.length);
|
|
1315
1001
|
this.header = restoredHeader ?? snapshotSessionHeader(id, header);
|
|
@@ -1318,8 +1004,10 @@ var Session = class Session {
|
|
|
1318
1004
|
const inheritedEventCount = SessionLogOffset(suppliedInheritedEventCount ?? 0);
|
|
1319
1005
|
if (!this.header.isSeeded && inheritedEventCount !== 0) throw new Error("unseeded session inherited event count must be 0");
|
|
1320
1006
|
if (inheritedEventCount > this.log.length) throw new Error("session inherited event count exceeds its event log");
|
|
1007
|
+
if (mode === "snapshot" && this.header.isSeeded && inheritedEventCount !== this.log.length) throw new Error("seeded session constructor seed must equal its inherited prefix");
|
|
1321
1008
|
this.inheritedEventCount = inheritedEventCount;
|
|
1322
|
-
if (seed !== void 0 &&
|
|
1009
|
+
if (seed !== void 0 && mode === "snapshot" && this.header.isSeeded) this.append("session/end-seed", { inherited: true });
|
|
1010
|
+
else if (seed !== void 0 && this.log.at(-1)?.type !== "session/end-seed") this.append("session/end-seed", {});
|
|
1323
1011
|
}
|
|
1324
1012
|
/** Cached immutable full snapshot of the private append-only log. */
|
|
1325
1013
|
eventsSnapshot;
|
|
@@ -1382,7 +1070,8 @@ var Session = class Session {
|
|
|
1382
1070
|
* declare how it joins the surface, the sole source of derived model
|
|
1383
1071
|
* history) and
|
|
1384
1072
|
* rejected by the compiler for non-surface types like `turn/start` or
|
|
1385
|
-
* `assistant/
|
|
1073
|
+
* `assistant/attempt`. Assistant messages embed their exact provider
|
|
1074
|
+
* stream and cannot cite top-level source events.
|
|
1386
1075
|
* @returns the logged event — its assigned `seq`/`time` plus the SNAPSHOT of
|
|
1387
1076
|
* `data` that entered the log, so reading `event.data` back sees the logged
|
|
1388
1077
|
* value, never the caller's still-mutable input.
|
|
@@ -1408,7 +1097,6 @@ var Session = class Session {
|
|
|
1408
1097
|
};
|
|
1409
1098
|
const dataSnapshot = snapshotJsonValue(data);
|
|
1410
1099
|
if (dataSnapshot === void 0) throw new Error(`session event "${type}" carries non-JSON-serializable data`);
|
|
1411
|
-
assertSupportedRequestHeader(type, dataSnapshot, `session event "${type}"`);
|
|
1412
1100
|
const surfaceMetadataSnapshot = snapshotJsonValue(surfaceMetadata);
|
|
1413
1101
|
if (surfaceMetadataSnapshot === void 0) throw new Error(`session event "${type}" carries non-JSON-serializable surface metadata`);
|
|
1414
1102
|
const entry = attachments.get(this);
|
|
@@ -1537,8 +1225,9 @@ var SessionForkError = class extends Error {
|
|
|
1537
1225
|
/**
|
|
1538
1226
|
* In-memory session store (`ctx.sessions`).
|
|
1539
1227
|
*
|
|
1540
|
-
* Persistence is intentionally not implemented here —
|
|
1541
|
-
*
|
|
1228
|
+
* Persistence is intentionally not implemented here — the agent lifecycle
|
|
1229
|
+
* attaches a session-log writer to each published session's write handle;
|
|
1230
|
+
* a session published outside that lifecycle persists nothing.
|
|
1542
1231
|
*/
|
|
1543
1232
|
var SessionStore = class extends Service {
|
|
1544
1233
|
store = /* @__PURE__ */ new Map();
|
|
@@ -1595,10 +1284,9 @@ var SessionStore = class extends Service {
|
|
|
1595
1284
|
*
|
|
1596
1285
|
* @param id - the session id; omitted, the store mints `session-<n>`.
|
|
1597
1286
|
* @param options - seed events and/or creation metadata for the header. With
|
|
1598
|
-
* `
|
|
1599
|
-
*
|
|
1600
|
-
*
|
|
1601
|
-
* retain no mutable aliases.
|
|
1287
|
+
* `eventState`, every seed event is either independently owned or any
|
|
1288
|
+
* shared value is deeply frozen; {@link Session.fromRestore} validates and
|
|
1289
|
+
* adopts those values without copying or freezing them.
|
|
1602
1290
|
* @returns the constructed session, NOT yet in the store.
|
|
1603
1291
|
* @throws if a session with `id` already exists, metadata is not a plain
|
|
1604
1292
|
* lossless-JSON record with valid scalar fields, or `meta.cwd` is a
|
|
@@ -1611,11 +1299,20 @@ var SessionStore = class extends Service {
|
|
|
1611
1299
|
while (this.store.has(sessionId));
|
|
1612
1300
|
else sessionId = brandString(id);
|
|
1613
1301
|
if (this.store.has(sessionId)) throw new Error(`session "${sessionId}" already exists`);
|
|
1614
|
-
if (options
|
|
1302
|
+
if (options !== void 0) {
|
|
1303
|
+
const { eventState } = options;
|
|
1304
|
+
switch (eventState) {
|
|
1305
|
+
case "detached":
|
|
1306
|
+
case "shared-frozen": return Session.fromRestore(sessionId, options.seed, options.meta, options.inheritedEventCount, eventState);
|
|
1307
|
+
case void 0: break;
|
|
1308
|
+
/* v8 ignore next -- closed-union exhaustiveness guard */
|
|
1309
|
+
default: assertNever(eventState, "SessionStore.prepare event state");
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1615
1312
|
const seed = options?.seed;
|
|
1616
1313
|
const meta = options?.meta;
|
|
1617
1314
|
const header = {
|
|
1618
|
-
version:
|
|
1315
|
+
version: 2,
|
|
1619
1316
|
id: sessionId,
|
|
1620
1317
|
createdAt: meta?.createdAt ?? Date.now(),
|
|
1621
1318
|
...meta?.cwd === void 0 ? {} : { cwd: meta.cwd },
|
|
@@ -1847,4 +1544,4 @@ var SessionStore = class extends Service {
|
|
|
1847
1544
|
}
|
|
1848
1545
|
};
|
|
1849
1546
|
//#endregion
|
|
1850
|
-
export { KNOWN_SESSION_EVENT_TYPES, SESSION_FORMAT_VERSION, Session, SessionForkError, SessionId, SessionLogOffset, SessionPreparation, SessionSeq, SessionStore, SessionStore as default, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN, adoptSessionEvent, canonicalHeader, decodeSeqRanges,
|
|
1547
|
+
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 };
|
package/lib/invariant.js
CHANGED
|
@@ -50,8 +50,8 @@ function validateEvent(trace, event, fail) {
|
|
|
50
50
|
openStep = null;
|
|
51
51
|
nextStep += 1;
|
|
52
52
|
break;
|
|
53
|
-
case "assistant/
|
|
54
|
-
requireOpenStep(trace, "assistant/
|
|
53
|
+
case "assistant/attempt":
|
|
54
|
+
requireOpenStep(trace, "assistant/attempt", event.data.turn, event.data.step, fail);
|
|
55
55
|
break;
|
|
56
56
|
case "assistant/message":
|
|
57
57
|
requireOpenStep(trace, "assistant/message", event.data.turn, event.data.step, fail);
|