@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/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
- * The on-disk session format version, stamped into every newly-written {@link SessionHeader}
36
- * and enforced by every persistence backend on load. The single source of truth for the
37
- * version write sites and the load-time check all read it.
38
- * While the harness is unreleased it is pinned at `0`: no compatibility is
39
- * implied, incompatible logs are rejected, and no migration is provided.
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 full mechanism
53
- * (upgrade-step chain, in-memory view conversion, migrate-on-continue) is
54
- * recorded in the session-log-version-mechanism Agent Note
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 = 0;
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 (chunk, boundary, log-only record) or an
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 && event.type !== "assistant/message") throw new Error("sourceEventSeqs must not be empty except on assistant/message");
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/chunk",
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 !== 0) throw new Error(`session header version must be 0, got ${String(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: 0,
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), so consumers
1256
- * that replay the log as a publication substitute (telemetry adoption)
1257
- * start here. Distinct from {@link inheritedEventCount}, the DURABLE
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 taking ownership of fresh persistence values.
1288
- * The storage format, event envelopes, sequence continuity, surface transitions,
1289
- * and header fields are validated before the restored objects are frozen.
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 - fresh detached events whose ownership is transferred.
1292
- * @param header - fresh detached metadata whose ownership is transferred.
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, "restore", inheritedEventCount);
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 === "restore" ? validateRestoredSessionHeader(id, header) : void 0;
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 === "restore" ? source : snapshotJsonValue(source);
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 === "restore" ? freezeRestoredObject(snapshot) : deepFreeze(snapshot));
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 && this.log.at(-1)?.type !== "session/end-seed") this.append("session/end-seed", {});
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/chunk`.
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 — persistence plugins
1541
- * subscribe to `session/event` and flush on `session/flush` / dispose.
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
- * `seedSource: 'persistence'`, metadata and events must be fresh detached
1599
- * graphs whose ownership transfers to this call: they are validated and
1600
- * frozen in place through {@link Session.fromRestore}, so the caller must
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?.seedSource === "persistence") return Session.fromRestore(sessionId, options.seed, options.meta, options.inheritedEventCount);
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: 0,
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, decodeStorageRecord, deriveEventMessage, encodeSeqRanges, foldRequestHeader, foldSurface, headerEquals, interruptedTurnClosers, isAppendSurfaceEvent, isReplacementSurfaceEvent, isSurfaceEligibleType, isSurfaceEvent, packChunkRuns, snapshotSessionEvent };
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/chunk":
54
- requireOpenStep(trace, "assistant/chunk", event.data.turn, event.data.step, fail);
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);