@deepseek-ai/dsh-session 0.1.2-alpha.2 → 0.1.2-alpha.4

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 CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/core/session/README.md
5
- README.md: 0d691b31c4918152ecf1092002646296c5d9984a
6
- README.zh.md: 2118def74c059f6637f12a9aeba20ff92a5d2363
5
+ README.md: f5cf910854203021a619cc786dfd13705927ffc1
6
+ README.zh.md: 385d7fd63a6e4dec9c23c9d38a352942d7dbc7f9
package/README.md CHANGED
@@ -49,10 +49,18 @@ session.deriveMessages() // the derived model history
49
49
 
50
50
  Surface events (`user/message`, `assistant/message`, `tool/result`) must declare how they join the ordered surface; raw chunks, boundaries, and other log-only events never produce a message.
51
51
 
52
+ ### Read the log
53
+
54
+ `session.seq` reads the current log length without materializing an array, and `session.eventAt(seq)` reads one accepted, deeply frozen event by sequence number. `session.snapshotEvents(fromSeq?, toSeqExclusive?)` materializes a frozen, stable snapshot of a half-open range; a complete current snapshot is cached until the next append. Callers that only need a length or one event use `seq` or `eventAt()`.
55
+
56
+ Session log positions use two numeric types. `SessionSeq` identifies an existing event or inclusive event watermark; `SessionLogOffset` identifies a gap, prefix length, or read boundary and may equal the event count. `SessionSeqCursor` adds the `-1` “no event yet” value, while `OptionalSessionSeq` uses `null` when absence is data. The constructors validate non-negative safe integers, and the brands disappear at runtime, so durable JSON and wire values remain ordinary numbers.
57
+
52
58
  ### Fork a session
53
59
 
54
60
  `ctx.sessions.fork(source, boundary?, childSessionId?)` selects source events through an inclusive `boundary` seq (default: the current last event), requires the prefix to end outside an open turn, and creates a live child session with lineage metadata. A tool-time delegation that must branch mid-turn clips to a completed prefix instead.
55
61
 
62
+ The logical `SessionHeader.isSeeded` field reports whether fork history exists without exposing a positional integer. `Session.inheritedEventCount` retains the exact checked `SessionLogOffset`; `ownEvents()` returns events at and after that cut, and `isOwnSeq(seq)` accepts only an existing child-owned position. A low-level seeded constructor must supply an explicit `seed` and `inheritedEventCount` because the constructor seed can contain child-owned setup events after the inherited prefix.
63
+
56
64
  ### Flush durable state
57
65
 
58
66
  `ctx.sessions.flush(session)` dispatches the awaited durability checkpoint: every persistence listener flushes and the call settles after all of them. A producer that needs an immediate durability barrier awaits it instead of assuming the write-behind drained.
@@ -90,7 +98,7 @@ The package is built on event sourcing: a `Session` is an append-only log of typ
90
98
 
91
99
  ### Append validation
92
100
 
93
- Every append uses the shared iterative `snapshotJsonValue()` pass, which reads, validates, and copies each nested value once, so a stateful getter cannot supply one value to validation and another to storage. Non-lossless-JSON payloads (BigInt, cycles, sparse arrays, `-0`, exotic prototypes) are rejected at the append site, before any backend flush. Surface events additionally validate marker shape, cited source-event seqs, and complete shadowed-node coverage for replacements.
101
+ Every append uses the shared iterative `snapshotJsonValue()` pass, which reads, validates, and copies each nested value once, so a stateful getter cannot supply one value to validation and another to storage. Non-lossless-JSON payloads (BigInt, cycles, sparse arrays, `-0`, exotic prototypes) are rejected at the append site, before any backend flush. The append path constructs each `SessionSeq`; surface events additionally validate marker shape, cited source-event sequences, and complete shadowed-node coverage for replacements.
94
102
 
95
103
  ### Derived history
96
104
 
package/README.zh.md CHANGED
@@ -49,10 +49,18 @@ session.deriveMessages() // the derived model history
49
49
 
50
50
  表层事件(`user/message`、`assistant/message`、`tool/result`)必须声明如何进入有序 surface;原始分片、边界与其他仅日志事件从不产生消息。
51
51
 
52
+ ### 读取日志
53
+
54
+ `session.seq` 无需物化数组即可读取当前日志长度,`session.eventAt(seq)` 按序列号读取单个已接受且深度冻结的事件。`session.snapshotEvents(fromSeq?, toSeqExclusive?)` 会物化半开区间的冻结稳定快照;当前完整快照会缓存到下一次追加。只需要长度或单个事件的调用方使用 `seq` 或 `eventAt()`。
55
+
56
+ 会话日志位置使用两种数字类型。`SessionSeq` 标识已有事件或包含端点的事件水位;`SessionLogOffset` 标识间隙、前缀长度或读取边界,并且可以等于事件数量。`SessionSeqCursor` 添加 `-1` 这个“尚无事件”值,`OptionalSessionSeq` 则在缺失本身属于数据时使用 `null`。构造函数会校验非负安全整数,品牌在运行时会被擦除,因此持久 JSON 与 wire 值仍是普通数字。
57
+
52
58
  ### 派生会话的 fork
53
59
 
54
60
  `ctx.sessions.fork(source, boundary?, childSessionId?)` 选取截至 `boundary` 事件序号(含该事件)的源事件(默认:当前最后一个事件),要求所选前缀结束时没有开放轮次,再创建带谱系元数据的实时子会话。必须在轮次中途分支的工具时委派会裁剪到已完成前缀。
55
61
 
62
+ 逻辑 `SessionHeader.isSeeded` 字段报告是否存在 fork 历史,而不公开位置整数。`Session.inheritedEventCount` 保留经过校验的精确 `SessionLogOffset`;`ownEvents()` 返回从该切点开始的事件,`isOwnSeq(seq)` 只接受已存在且由 child 拥有的位置。底层带 seed 构造必须显式提供 `seed` 与 `inheritedEventCount`,因为构造 seed 可以在继承前缀之后包含 child 自有的设置事件。
63
+
56
64
  ### 刷新持久状态
57
65
 
58
66
  `ctx.sessions.flush(session)` 分发需等待完成的持久性检查点:每个持久化监听器都会刷新,调用在所有监听器结算后完成。需要立即持久性屏障的生产方应等待它,而不是假定写后刷新已完成。
@@ -90,7 +98,7 @@ session.deriveMessages() // the derived model history
90
98
 
91
99
  ### 追加校验
92
100
 
93
- 每次追加都会使用共享的迭代式 `snapshotJsonValue()` 流程,对每个嵌套值只读取、校验并复制一次,因此有状态的 getter 无法给校验提供一个值、给存储提供另一个值。非无损 JSON 载荷(BigInt、循环、稀疏数组、`-0`、特殊原型)会在追加位置被拒绝,先于任何后端刷新。表层事件还会校验标记形态、被引用的源事件 seq,以及替换的完整遮蔽节点覆盖。
101
+ 每次追加都会使用共享的迭代式 `snapshotJsonValue()` 流程,对每个嵌套值只读取、校验并复制一次,因此有状态的 getter 无法给校验提供一个值、给存储提供另一个值。非无损 JSON 载荷(BigInt、循环、稀疏数组、`-0`、特殊原型)会在追加位置被拒绝,先于任何后端刷新。追加路径会构造每个 `SessionSeq`;surface 事件还会校验标记形态、被引用的源事件序号,以及替换的完整遮蔽节点覆盖。
94
102
 
95
103
  ### 派生历史
96
104
 
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Service } from "@deepseek-ai/cordis";
2
2
  import { isAbsolute } from "node:path";
3
- import { brandString } from "@deepseek-ai/dsh-brand";
3
+ import { brandNumber, brandString } from "@deepseek-ai/dsh-brand";
4
4
  import { 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";
@@ -14,6 +14,24 @@ function SessionId(id) {
14
14
  return brandString(id);
15
15
  }
16
16
  /**
17
+ * Admit a numeric value as an existing Session event position.
18
+ * @param value - non-negative safe integer admitted by the owning log operation.
19
+ * @returns the same number with the Session-sequence brand.
20
+ */
21
+ function SessionSeq(value) {
22
+ if (!Number.isSafeInteger(value) || value < 0 || Object.is(value, -0)) throw new TypeError(`SessionSeq must be a non-negative safe integer, got ${String(value)}`);
23
+ return brandNumber(value);
24
+ }
25
+ /**
26
+ * Admit a numeric value as a Session log offset.
27
+ * @param value - non-negative safe integer used as a gap or prefix length.
28
+ * @returns the same number with the Session-log-offset brand.
29
+ */
30
+ function SessionLogOffset(value) {
31
+ if (!Number.isSafeInteger(value) || value < 0 || Object.is(value, -0)) throw new TypeError(`SessionLogOffset must be a non-negative safe integer, got ${String(value)}`);
32
+ return brandNumber(value);
33
+ }
34
+ /**
17
35
  * The on-disk session format version, stamped into every newly-written {@link SessionHeader}
18
36
  * and enforced by every persistence backend on load. The single source of truth for the
19
37
  * version — write sites and the load-time check all read it.
@@ -127,7 +145,7 @@ function createFoldState() {
127
145
  }
128
146
  /** Whether a runtime value is a non-negative safe event sequence. */
129
147
  function isEventSeq(value) {
130
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
148
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && !Object.is(value, -0);
131
149
  }
132
150
  /** Whether a runtime value is the exact positional-replacement shape. */
133
151
  function isReplaceOp(value) {
@@ -278,7 +296,7 @@ function foldSurface(events) {
278
296
  const state = createFoldState();
279
297
  const replacements = [];
280
298
  for (const [index, event] of events.entries()) {
281
- const replacement = applySurfaceEvent(state, event, index, events, 0);
299
+ const replacement = applySurfaceEvent(state, event, SessionSeq(index), events, SessionLogOffset(0));
282
300
  if (replacement !== void 0) replacements.push(replacement);
283
301
  }
284
302
  return {
@@ -300,10 +318,10 @@ var SurfaceManager = class {
300
318
  * @param log - Contiguous complete log or loaded event window.
301
319
  * @param baseSeq - Absolute sequence of the window's first event.
302
320
  */
303
- constructor(log, baseSeq = 0) {
321
+ constructor(log, baseSeq = SessionLogOffset(0)) {
304
322
  this.log = log;
305
323
  this.baseSeq = baseSeq;
306
- this._lastProcessedSeq = baseSeq - 1;
324
+ this._lastProcessedSeq = baseSeq === 0 ? -1 : SessionSeq(baseSeq - 1);
307
325
  }
308
326
  /**
309
327
  * Validate the next candidate without mutating the committed surface.
@@ -311,7 +329,7 @@ var SurfaceManager = class {
311
329
  */
312
330
  validateNext(event) {
313
331
  if (this._lastProcessedSeq < this.baseSeq + this.log.length - 1) this._processDelta();
314
- const expectedSeq = this.baseSeq + this.log.length;
332
+ const expectedSeq = SessionSeq(this.baseSeq + this.log.length);
315
333
  this._pendingPlan = {
316
334
  event,
317
335
  expectedSeq,
@@ -336,9 +354,9 @@ var SurfaceManager = class {
336
354
  const event = this.log[index];
337
355
  const pending = this._pendingPlan;
338
356
  if (pending?.event === event && pending.expectedSeq === seq) applySurfacePlan(this._state, pending.plan);
339
- else applySurfaceEvent(this._state, event, seq, this.log, this.baseSeq);
357
+ else applySurfaceEvent(this._state, event, SessionSeq(seq), this.log, this.baseSeq);
340
358
  if (pending !== void 0 && pending.expectedSeq <= seq) this._pendingPlan = void 0;
341
- this._lastProcessedSeq = seq;
359
+ this._lastProcessedSeq = SessionSeq(seq);
342
360
  }
343
361
  }
344
362
  };
@@ -518,7 +536,7 @@ function interruptedTurnClosers(events) {
518
536
  });
519
537
  closers.push({
520
538
  type: "tool/result",
521
- seq: seq++,
539
+ seq: SessionSeq(seq++),
522
540
  time,
523
541
  data: {
524
542
  turn: openTurn,
@@ -538,7 +556,7 @@ function interruptedTurnClosers(events) {
538
556
  }
539
557
  if (openStep !== null) closers.push({
540
558
  type: "step/end",
541
- seq: seq++,
559
+ seq: SessionSeq(seq++),
542
560
  time,
543
561
  data: {
544
562
  turn: openTurn,
@@ -547,7 +565,7 @@ function interruptedTurnClosers(events) {
547
565
  });
548
566
  closers.push({
549
567
  type: "turn/end",
550
- seq: seq++,
568
+ seq: SessionSeq(seq++),
551
569
  time,
552
570
  data: {
553
571
  turn: openTurn,
@@ -567,7 +585,7 @@ function interruptedTurnClosers(events) {
567
585
  * `tool-call-chunks` — and expands rows back to the exact original events.
568
586
  *
569
587
  * Packed rows are an encoding vocabulary, NOT session events: they never enter
570
- * `Session.events`, have no `SessionEventMap` entry, and use bare (slash-less)
588
+ * `Session.snapshotEvents()`, have no `SessionEventMap` entry, and use bare (slash-less)
571
589
  * type tags so a reader cannot confuse them with the event taxonomy
572
590
  * (precedent: the JSONL header line's `session` tag). Persistence and bounded
573
591
  * history transport both use the codec. The encoder whitelists exact shapes —
@@ -607,7 +625,7 @@ function classify(event) {
607
625
  "time",
608
626
  "data"
609
627
  ])) return void 0;
610
- if (!Number.isSafeInteger(event.seq) || event.seq < 0 || !Number.isSafeInteger(event.time)) return void 0;
628
+ if (!Number.isSafeInteger(event.seq) || event.seq < 0 || Object.is(event.seq, -0) || !Number.isSafeInteger(event.time)) return void 0;
611
629
  const data = event.data;
612
630
  if (!isRecord(data) || !hasExactKeys(data, [
613
631
  "turn",
@@ -760,7 +778,7 @@ function validateRow(value, tag) {
760
778
  "time0",
761
779
  "data"
762
780
  ])) malformed(tag, "envelope must be exactly {type, seq0, time0, data}");
763
- if (!Number.isSafeInteger(value.seq0) || value.seq0 < 0) malformed(tag, "seq0 must be a non-negative safe integer");
781
+ if (!Number.isSafeInteger(value.seq0) || value.seq0 < 0 || Object.is(value.seq0, -0)) malformed(tag, "seq0 must be a non-negative safe integer");
764
782
  if (!Number.isSafeInteger(value.time0)) malformed(tag, "time0 must be a safe integer");
765
783
  const data = value.data;
766
784
  if (!isRecord(data)) malformed(tag, "data must be an object");
@@ -801,6 +819,7 @@ function validateRow(value, tag) {
801
819
  time += gap;
802
820
  if (!Number.isSafeInteger(time)) malformed(tag, "member times must stay safe integers");
803
821
  }
822
+ SessionSeq(value.seq0);
804
823
  return value;
805
824
  }
806
825
  /** Expand a validated row back into its exact original events, in order. */
@@ -840,7 +859,7 @@ function expandRow(row) {
840
859
  }
841
860
  events.push({
842
861
  type: "assistant/chunk",
843
- seq: row.seq0 + k,
862
+ seq: SessionSeq(row.seq0 + k),
844
863
  time,
845
864
  data: {
846
865
  turn: row.data.turn,
@@ -855,7 +874,8 @@ function expandRow(row) {
855
874
  * Decode one parsed JSONL line value into the session event(s) it stores.
856
875
  * Chunk-row-tagged values validate and expand (a malformed row throws — it is
857
876
  * corrupt storage, and treating it as an event would silently drop a whole
858
- * run); every other value passes through as a single event, unvalidated.
877
+ * run); every other value passes through as a single event after admitting a
878
+ * numeric `seq` through the Session-sequence constructor.
859
879
  *
860
880
  * @param value - one line's `JSON.parse` result.
861
881
  * @returns the stored events, in log order.
@@ -863,7 +883,10 @@ function expandRow(row) {
863
883
  function decodeStorageRecord(value) {
864
884
  if (!isRecord(value)) return [value];
865
885
  const tag = value.type;
866
- if (tag !== "text-chunks" && tag !== "reasoning-chunks" && tag !== "tool-call-chunks") return [value];
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
+ }
867
890
  return expandRow(validateRow(value, tag));
868
891
  }
869
892
  //#endregion
@@ -978,7 +1001,7 @@ function decodeSeqRanges(value, maxEntries = Number.MAX_SAFE_INTEGER) {
978
1001
  if (typeof entry === "number") {
979
1002
  assertSeq(entry);
980
1003
  if (decoded.length >= maxEntries) throw new TypeError("sourceEventSeqs exceeds its event sequence");
981
- decoded.push(entry);
1004
+ decoded.push(SessionSeq(entry));
982
1005
  continue;
983
1006
  }
984
1007
  if (!Array.isArray(entry) || entry.length !== 2) throw new TypeError("sourceEventSeqs range entries must be [start, end] pairs");
@@ -988,7 +1011,7 @@ function decodeSeqRanges(value, maxEntries = Number.MAX_SAFE_INTEGER) {
988
1011
  assertSeq(end);
989
1012
  if (end < start) throw new TypeError("sourceEventSeqs ranges require start <= end");
990
1013
  if (end - start + 1 > maxEntries - decoded.length) throw new TypeError("sourceEventSeqs range exceeds its event sequence");
991
- for (let seq = start; seq <= end; seq += 1) decoded.push(seq);
1014
+ for (let seq = start; seq <= end; seq += 1) decoded.push(SessionSeq(seq));
992
1015
  hasRange = true;
993
1016
  }
994
1017
  if (hasRange && !isStrictlyIncreasing(decoded)) throw new TypeError("sourceEventSeqs ranges must be strictly increasing");
@@ -1010,6 +1033,7 @@ function assertSeq(value) {
1010
1033
  function validateSessionHeader(id, input) {
1011
1034
  if (input === null || typeof input !== "object" || Array.isArray(input)) throw new Error("session header is not a plain JSON record");
1012
1035
  const record = input;
1036
+ if (Object.hasOwn(record, "seedLength")) throw new Error("session header has invalid field \"seedLength\"");
1013
1037
  if (record.version !== 0) throw new Error(`session header version must be 0, got ${String(record.version)}`);
1014
1038
  if (record.id !== id) throw new Error(`session header id "${String(record.id)}" does not match session id "${id}"`);
1015
1039
  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");
@@ -1018,7 +1042,7 @@ function validateSessionHeader(id, input) {
1018
1042
  if (!isAbsolute(record.cwd)) throw new Error(`session header cwd must be an absolute path, got "${record.cwd}"`);
1019
1043
  }
1020
1044
  if (record.parentSession !== void 0 && typeof record.parentSession !== "string") throw new Error("session header parentSession must be a string");
1021
- if (record.seedLength !== void 0 && (typeof record.seedLength !== "number" || !Number.isSafeInteger(record.seedLength) || record.seedLength < 0)) throw new Error("session header seedLength must be a non-negative safe integer");
1045
+ if (typeof record.isSeeded !== "boolean") throw new Error("session header isSeeded must be a boolean");
1022
1046
  if (record.origin !== void 0 && record.origin !== "subagent") throw new Error("session header origin must be \"subagent\"");
1023
1047
  if (record.delegationDepth !== void 0 && (typeof record.delegationDepth !== "number" || !Number.isSafeInteger(record.delegationDepth) || record.delegationDepth < 0)) throw new Error("session header delegationDepth must be a non-negative safe integer");
1024
1048
  if (record.agentPreset !== void 0 && typeof record.agentPreset !== "string") throw new Error("session header agentPreset must be a string");
@@ -1037,7 +1061,8 @@ function snapshotSessionHeader(id, source) {
1037
1061
  const snapshot = snapshotJsonValue(source === void 0 ? {
1038
1062
  version: 0,
1039
1063
  id,
1040
- createdAt: Date.now()
1064
+ createdAt: Date.now(),
1065
+ isSeeded: false
1041
1066
  } : source);
1042
1067
  if (snapshot === void 0) throw new Error("session header is not losslessly JSON-serializable");
1043
1068
  return validateSessionHeader(id, snapshot);
@@ -1102,7 +1127,7 @@ function assertSessionEventEnvelope(value, index) {
1102
1127
  const type = event["type"];
1103
1128
  const seq = event["seq"];
1104
1129
  const time = event["time"];
1105
- if (typeof type !== "string" || typeof seq !== "number" || !Number.isSafeInteger(seq) || seq < 0 || typeof time !== "number" || !Number.isSafeInteger(time) || event["data"] === void 0 || event["ignorable"] !== void 0 && event["ignorable"] !== true) throw new Error(`seed event at index ${index} has an invalid event envelope`);
1130
+ if (typeof type !== "string" || typeof seq !== "number" || !Number.isSafeInteger(seq) || seq < 0 || Object.is(seq, -0) || typeof time !== "number" || !Number.isSafeInteger(time) || event["data"] === void 0 || event["ignorable"] !== void 0 && event["ignorable"] !== true) throw new Error(`seed event at index ${index} has an invalid event envelope`);
1106
1131
  switch (type) {
1107
1132
  case "request/header":
1108
1133
  case "user/message":
@@ -1210,13 +1235,15 @@ var Session = class Session {
1210
1235
  }
1211
1236
  /**
1212
1237
  * Detached, deep-frozen creation metadata (format version, cwd, lineage,
1213
- * seed boundary). Supplied by the store via `ctx.sessions.create()`. When a
1238
+ * and whether fork history exists). Supplied by the store via `ctx.sessions.create()`. When a
1214
1239
  * `Session` is created without a store-owned header, a minimal header is
1215
1240
  * synthesized (stamped with the current {@link SESSION_FORMAT_VERSION}) so
1216
1241
  * `session.header` is always present. Kept out of the event log — it is a
1217
1242
  * storage concern, not replayable conversation state.
1218
1243
  */
1219
1244
  header;
1245
+ /** Number of leading events inherited from this Session's fork parent. */
1246
+ inheritedEventCount;
1220
1247
  /** The session identity, derived from its durable header's single copy. */
1221
1248
  get id() {
1222
1249
  return this.header.id;
@@ -1227,9 +1254,9 @@ var Session = class Session {
1227
1254
  * construction — replay, fork, or resume — and were never published on the
1228
1255
  * `session/event` firehose (constructor seeds do not emit), so consumers
1229
1256
  * that replay the log as a publication substitute (telemetry adoption)
1230
- * start here. Distinct from `header.seedLength`, the DURABLE fork-lineage
1231
- * boundary: a resumed session's constructor seed is its full stored log,
1232
- * while its header keeps the original fork value — this field is the
1257
+ * start here. Distinct from {@link inheritedEventCount}, the DURABLE
1258
+ * fork-lineage cut: a resumed session's constructor seed is its full stored
1259
+ * log, while the inherited count keeps the original fork value — this field is the
1233
1260
  * in-process construction fact.
1234
1261
  *
1235
1262
  * Not persisted itself: a seeded session projects it into the log as the
@@ -1250,10 +1277,11 @@ var Session = class Session {
1250
1277
  * @param id - session identity.
1251
1278
  * @param seed - optional borrowed replay or fork events.
1252
1279
  * @param header - optional borrowed storage metadata.
1280
+ * @param inheritedEventCount - exact fork-inherited prefix length for a seeded header.
1253
1281
  * @returns a detached session.
1254
1282
  */
1255
- static create(id, seed, header) {
1256
- return new Session(id, seed, header);
1283
+ static create(id, seed, header, inheritedEventCount) {
1284
+ return new Session(id, seed, header, "snapshot", inheritedEventCount);
1257
1285
  }
1258
1286
  /**
1259
1287
  * Restore a detached session by taking ownership of fresh persistence values.
@@ -1262,12 +1290,13 @@ var Session = class Session {
1262
1290
  * @param id - restored session identity.
1263
1291
  * @param seed - fresh detached events whose ownership is transferred.
1264
1292
  * @param header - fresh detached metadata whose ownership is transferred.
1293
+ * @param inheritedEventCount - exact fork-inherited prefix length decoded from storage.
1265
1294
  * @returns a restored detached session.
1266
1295
  */
1267
- static fromRestore(id, seed, header) {
1268
- return new Session(id, seed, header, "restore");
1296
+ static fromRestore(id, seed, header, inheritedEventCount) {
1297
+ return new Session(id, seed, header, "restore", inheritedEventCount);
1269
1298
  }
1270
- constructor(id, seed, header, mode = "snapshot") {
1299
+ constructor(id, seed, header, mode = "snapshot", suppliedInheritedEventCount) {
1271
1300
  const restoredHeader = mode === "restore" ? validateRestoredSessionHeader(id, header) : void 0;
1272
1301
  if (seed !== void 0) for (const [index, source] of seed.entries()) {
1273
1302
  const snapshot = mode === "restore" ? source : snapshotJsonValue(source);
@@ -1282,25 +1311,59 @@ var Session = class Session {
1282
1311
  }
1283
1312
  this.log.push(mode === "restore" ? freezeRestoredObject(snapshot) : deepFreeze(snapshot));
1284
1313
  }
1285
- this.firstLiveSeq = this.log.length;
1314
+ this.firstLiveSeq = SessionLogOffset(this.log.length);
1286
1315
  this.header = restoredHeader ?? snapshotSessionHeader(id, header);
1316
+ if (this.header.isSeeded && seed === void 0) throw new Error("seeded session requires an explicit constructor seed");
1317
+ if (this.header.isSeeded && suppliedInheritedEventCount === void 0) throw new Error("seeded session requires an inherited event count");
1318
+ const inheritedEventCount = SessionLogOffset(suppliedInheritedEventCount ?? 0);
1319
+ if (!this.header.isSeeded && inheritedEventCount !== 0) throw new Error("unseeded session inherited event count must be 0");
1320
+ if (inheritedEventCount > this.log.length) throw new Error("session inherited event count exceeds its event log");
1321
+ this.inheritedEventCount = inheritedEventCount;
1287
1322
  if (seed !== void 0 && this.log.at(-1)?.type !== "session/end-seed") this.append("session/end-seed", {});
1288
1323
  }
1289
- /** Cached immutable public snapshot of the private append-only log. */
1324
+ /** Cached immutable full snapshot of the private append-only log. */
1290
1325
  eventsSnapshot;
1291
1326
  /**
1292
- * An immutable snapshot of the append-only event log. The snapshot is reused
1293
- * until the next append; a previously returned array does not grow later.
1294
- * Events and their nested data are deep-frozen at acceptance, so neither a
1295
- * cast nor ordinary JavaScript can rewrite durable history.
1327
+ * Return the immutable event stored at one exact sequence number.
1328
+ * @param seq - event sequence number.
1329
+ * @returns the accepted event, or undefined when the log does not contain it.
1330
+ */
1331
+ eventAt(seq) {
1332
+ return this.log[seq];
1333
+ }
1334
+ /**
1335
+ * Materialize an immutable snapshot of a half-open event sequence range.
1336
+ * A full current snapshot is reused until the next append; every previously
1337
+ * returned snapshot remains stable after later appends.
1338
+ * @param fromSeq - non-negative inclusive sequence number; defaults to the log start.
1339
+ * @param toSeqExclusive - non-negative exclusive sequence number; defaults to the current end.
1340
+ * @returns a frozen array of the selected deeply frozen events.
1341
+ */
1342
+ snapshotEvents(fromSeq = SessionLogOffset(0), toSeqExclusive = this.seq) {
1343
+ if (fromSeq === 0 && toSeqExclusive === this.log.length) {
1344
+ this.eventsSnapshot ??= Object.freeze([...this.log]);
1345
+ return this.eventsSnapshot;
1346
+ }
1347
+ return Object.freeze(this.log.slice(fromSeq, toSeqExclusive));
1348
+ }
1349
+ /**
1350
+ * Return this Session's events after its fork-inherited prefix.
1351
+ * @returns a fresh array containing child-owned events in log order.
1352
+ */
1353
+ ownEvents() {
1354
+ return this.snapshotEvents(this.inheritedEventCount);
1355
+ }
1356
+ /**
1357
+ * Whether one existing event position is outside the fork-inherited prefix.
1358
+ * @param seq - event position in this Session.
1359
+ * @returns true when the event belongs to this Session rather than its parent.
1296
1360
  */
1297
- get events() {
1298
- this.eventsSnapshot ??= Object.freeze([...this.log]);
1299
- return this.eventsSnapshot;
1361
+ isOwnSeq(seq) {
1362
+ return seq >= this.inheritedEventCount && seq < this.seq;
1300
1363
  }
1301
1364
  /** The next event's sequence number — always the log length (the `seq = log.length` contiguity contract). */
1302
1365
  get seq() {
1303
- return this.log.length;
1366
+ return SessionLogOffset(this.log.length);
1304
1367
  }
1305
1368
  /**
1306
1369
  * Append one typed event to the log and synchronously notify observers via
@@ -1352,7 +1415,7 @@ var Session = class Session {
1352
1415
  if (entry?.appending) throw new Error("session append cannot reenter while another append is being published");
1353
1416
  const event = deepFreeze({
1354
1417
  type,
1355
- seq: this.log.length,
1418
+ seq: SessionSeq(this.log.length),
1356
1419
  time: Date.now(),
1357
1420
  data: dataSnapshot,
1358
1421
  ...surfaceMetadataSnapshot
@@ -1386,7 +1449,7 @@ var Session = class Session {
1386
1449
  * The {@link EpochHeader} in force after the log's last header event — the
1387
1450
  * header the NEXT request will be compared against — or undefined before
1388
1451
  * the first `request/header` snapshot. The live, incrementally-maintained
1389
- * form of `foldRequestHeader(session.events)`: each header event is folded
1452
+ * form of `foldRequestHeader(session.snapshotEvents())`: each header event is folded
1390
1453
  * once, when first seen, so a per-step read costs O(new events).
1391
1454
  * @returns the folded header, or undefined when no header event exists yet.
1392
1455
  */
@@ -1548,7 +1611,7 @@ var SessionStore = class extends Service {
1548
1611
  while (this.store.has(sessionId));
1549
1612
  else sessionId = brandString(id);
1550
1613
  if (this.store.has(sessionId)) throw new Error(`session "${sessionId}" already exists`);
1551
- if (options?.seedSource === "persistence") return Session.fromRestore(sessionId, options.seed, options.meta);
1614
+ if (options?.seedSource === "persistence") return Session.fromRestore(sessionId, options.seed, options.meta, options.inheritedEventCount);
1552
1615
  const seed = options?.seed;
1553
1616
  const meta = options?.meta;
1554
1617
  const header = {
@@ -1557,12 +1620,12 @@ var SessionStore = class extends Service {
1557
1620
  createdAt: meta?.createdAt ?? Date.now(),
1558
1621
  ...meta?.cwd === void 0 ? {} : { cwd: meta.cwd },
1559
1622
  ...meta?.parentSession === void 0 ? {} : { parentSession: meta.parentSession },
1560
- ...meta?.seedLength === void 0 ? {} : { seedLength: meta.seedLength },
1623
+ isSeeded: meta?.isSeeded ?? false,
1561
1624
  ...meta?.origin === void 0 ? {} : { origin: meta.origin },
1562
1625
  ...meta?.delegationDepth === void 0 ? {} : { delegationDepth: meta.delegationDepth },
1563
1626
  ...meta?.agentPreset === void 0 ? {} : { agentPreset: meta.agentPreset }
1564
1627
  };
1565
- return Session.create(sessionId, seed, header);
1628
+ return Session.create(sessionId, seed, header, options?.inheritedEventCount);
1566
1629
  }
1567
1630
  /**
1568
1631
  * Enter a {@link prepare}d session into the store: install the module-private
@@ -1743,16 +1806,16 @@ var SessionStore = class extends Service {
1743
1806
  const seed = this._forkSeed(liveSource, boundary);
1744
1807
  return this.create(childSessionId, {
1745
1808
  seed,
1809
+ inheritedEventCount: SessionLogOffset(seed.length),
1746
1810
  meta: {
1747
1811
  ...liveSource.header.cwd !== void 0 ? { cwd: liveSource.header.cwd } : {},
1748
1812
  parentSession: liveSource.id,
1749
- seedLength: seed.length
1813
+ isSeeded: true
1750
1814
  }
1751
1815
  });
1752
1816
  }
1753
1817
  _forkSeed(session, requestedBoundary) {
1754
- const events = session.events;
1755
- const lastEvent = events.at(-1);
1818
+ const lastEvent = session.snapshotEvents().at(-1);
1756
1819
  let boundary;
1757
1820
  if (requestedBoundary !== void 0) boundary = requestedBoundary;
1758
1821
  else {
@@ -1760,15 +1823,16 @@ var SessionStore = class extends Service {
1760
1823
  boundary = lastEvent.seq;
1761
1824
  }
1762
1825
  if (!Number.isSafeInteger(boundary) || boundary < 0) throw new SessionForkError(`fork boundary for session "${session.id}" must be a non-negative safe integer, got ${String(boundary)}`, "INVALID_BOUNDARY");
1763
- if (boundary >= events.length) {
1764
- const lastSeq = events.at(-1)?.seq;
1826
+ if (boundary >= session.seq) {
1827
+ const lastSeq = lastEvent?.seq;
1765
1828
  throw new SessionForkError(`fork boundary ${boundary} does not exist in session "${session.id}" (last seq: ${lastSeq ?? "none"})`, "INVALID_BOUNDARY");
1766
1829
  }
1767
- const boundaryEvent = events[boundary];
1830
+ const boundaryEvent = session.eventAt(boundary);
1768
1831
  if (boundaryEvent === void 0 || boundaryEvent.seq !== boundary) throw new SessionForkError(`fork boundary ${boundary} does not match a contiguous event seq in session "${session.id}"`, "INVALID_BOUNDARY");
1769
- const lastTurnBoundary = events.slice(0, boundary + 1).findLast((event) => event.type === "turn/start" || event.type === "turn/end");
1832
+ const events = session.snapshotEvents(SessionLogOffset(0), SessionLogOffset(boundary + 1));
1833
+ const lastTurnBoundary = events.findLast((event) => event.type === "turn/start" || event.type === "turn/end");
1770
1834
  if (lastTurnBoundary?.type === "turn/start") throw new SessionForkError(`fork boundary ${boundary} in session "${session.id}" ends inside open turn ${lastTurnBoundary.data.turn}`, "OPEN_TURN");
1771
- return events.slice(0, boundary + 1);
1835
+ return events;
1772
1836
  }
1773
1837
  _resolveForkSource(source) {
1774
1838
  if (typeof source === "string") {
@@ -1783,4 +1847,4 @@ var SessionStore = class extends Service {
1783
1847
  }
1784
1848
  };
1785
1849
  //#endregion
1786
- export { KNOWN_SESSION_EVENT_TYPES, SESSION_FORMAT_VERSION, Session, SessionForkError, SessionId, SessionPreparation, 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 };
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 };
package/lib/invariant.js CHANGED
@@ -130,7 +130,7 @@ const install = Object.assign((ctx, fail) => {
130
130
  const seedSession = (session) => {
131
131
  const trace = freshTrace();
132
132
  traces.set(session, trace);
133
- for (const event of session.events) applyTransition(trace, validateEvent(trace, event, fail));
133
+ for (const event of session.snapshotEvents()) applyTransition(trace, validateEvent(trace, event, fail));
134
134
  return trace;
135
135
  };
136
136
  /* v8 ignore next -- session/event always follows list() or session/created seeding */
@@ -7,7 +7,7 @@
7
7
  * `tool-call-chunks` — and expands rows back to the exact original events.
8
8
  *
9
9
  * Packed rows are an encoding vocabulary, NOT session events: they never enter
10
- * `Session.events`, have no `SessionEventMap` entry, and use bare (slash-less)
10
+ * `Session.snapshotEvents()`, have no `SessionEventMap` entry, and use bare (slash-less)
11
11
  * type tags so a reader cannot confuse them with the event taxonomy
12
12
  * (precedent: the JSONL header line's `session` tag). Persistence and bounded
13
13
  * history transport both use the codec. The encoder whitelists exact shapes —
@@ -19,7 +19,7 @@
19
19
  * @module @deepseek-ai/dsh-session/chunk-rows
20
20
  */
21
21
  import type { ToolCallId } from '@deepseek-ai/dsh-llm/brand';
22
- import type { SessionEvent } from './types.ts';
22
+ import type { SessionEvent, SessionSeq as SessionSeqType } from './types.ts';
23
23
  /**
24
24
  * Fields shared by every packed run: placement, block correlation, and member
25
25
  * timestamps as gaps. Member `k` reconstructs as seq `seq0 + k` and time
@@ -52,17 +52,17 @@ interface ToolCallRunData extends RunDataBase {
52
52
  */
53
53
  export type ChunkRow = {
54
54
  type: 'text-chunks';
55
- seq0: number;
55
+ seq0: SessionSeqType;
56
56
  time0: number;
57
57
  data: TextRunData;
58
58
  } | {
59
59
  type: 'reasoning-chunks';
60
- seq0: number;
60
+ seq0: SessionSeqType;
61
61
  time0: number;
62
62
  data: TextRunData;
63
63
  } | {
64
64
  type: 'tool-call-chunks';
65
- seq0: number;
65
+ seq0: SessionSeqType;
66
66
  time0: number;
67
67
  data: ToolCallRunData;
68
68
  };
@@ -95,7 +95,8 @@ export declare function packChunkRuns(events: readonly SessionEvent[]): StorageR
95
95
  * Decode one parsed JSONL line value into the session event(s) it stores.
96
96
  * Chunk-row-tagged values validate and expand (a malformed row throws — it is
97
97
  * corrupt storage, and treating it as an event would silently drop a whole
98
- * run); every other value passes through as a single event, unvalidated.
98
+ * run); every other value passes through as a single event after admitting a
99
+ * numeric `seq` through the Session-sequence constructor.
99
100
  *
100
101
  * @param value - one line's `JSON.parse` result.
101
102
  * @returns the stored events, in log order.
@@ -7,7 +7,7 @@
7
7
  * `tool-call-chunks` — and expands rows back to the exact original events.
8
8
  *
9
9
  * Packed rows are an encoding vocabulary, NOT session events: they never enter
10
- * `Session.events`, have no `SessionEventMap` entry, and use bare (slash-less)
10
+ * `Session.snapshotEvents()`, have no `SessionEventMap` entry, and use bare (slash-less)
11
11
  * type tags so a reader cannot confuse them with the event taxonomy
12
12
  * (precedent: the JSONL header line's `session` tag). Persistence and bounded
13
13
  * history transport both use the codec. The encoder whitelists exact shapes —
@@ -19,6 +19,7 @@
19
19
  * @module @deepseek-ai/dsh-session/chunk-rows
20
20
  */
21
21
  import { brandString } from '@deepseek-ai/dsh-brand';
22
+ import { SessionSeq } from "./types.js";
22
23
  /**
23
24
  * Test whether an encoded record is a packed chunk row rather than a Session event.
24
25
  * @param record - one persistence or bounded-history encoding record.
@@ -63,7 +64,8 @@ function classify(event) {
63
64
  return undefined;
64
65
  if (!hasExactKeys(event, ['type', 'seq', 'time', 'data']))
65
66
  return undefined;
66
- if (!Number.isSafeInteger(event.seq) || event.seq < 0 || !Number.isSafeInteger(event.time))
67
+ if (!Number.isSafeInteger(event.seq) || event.seq < 0 || Object.is(event.seq, -0)
68
+ || !Number.isSafeInteger(event.time))
67
69
  return undefined;
68
70
  const data = event.data;
69
71
  if (!isRecord(data) || !hasExactKeys(data, ['turn', 'step', 'chunk']))
@@ -219,7 +221,7 @@ function validateRow(value, tag) {
219
221
  if (!hasExactKeys(value, ['type', 'seq0', 'time0', 'data'])) {
220
222
  malformed(tag, 'envelope must be exactly {type, seq0, time0, data}');
221
223
  }
222
- if (!Number.isSafeInteger(value.seq0) || value.seq0 < 0) {
224
+ if (!Number.isSafeInteger(value.seq0) || value.seq0 < 0 || Object.is(value.seq0, -0)) {
223
225
  malformed(tag, 'seq0 must be a non-negative safe integer');
224
226
  }
225
227
  if (!Number.isSafeInteger(value.time0)) {
@@ -259,6 +261,7 @@ function validateRow(value, tag) {
259
261
  if (!Number.isSafeInteger(time))
260
262
  malformed(tag, 'member times must stay safe integers');
261
263
  }
264
+ SessionSeq(value.seq0);
262
265
  return value;
263
266
  }
264
267
  /** Expand a validated row back into its exact original events, in order. */
@@ -294,7 +297,7 @@ function expandRow(row) {
294
297
  }
295
298
  events.push({
296
299
  type: 'assistant/chunk',
297
- seq: row.seq0 + k,
300
+ seq: SessionSeq(row.seq0 + k),
298
301
  time,
299
302
  data: { turn: row.data.turn, step: row.data.step, chunk },
300
303
  });
@@ -305,7 +308,8 @@ function expandRow(row) {
305
308
  * Decode one parsed JSONL line value into the session event(s) it stores.
306
309
  * Chunk-row-tagged values validate and expand (a malformed row throws — it is
307
310
  * corrupt storage, and treating it as an event would silently drop a whole
308
- * run); every other value passes through as a single event, unvalidated.
311
+ * run); every other value passes through as a single event after admitting a
312
+ * numeric `seq` through the Session-sequence constructor.
309
313
  *
310
314
  * @param value - one line's `JSON.parse` result.
311
315
  * @returns the stored events, in log order.
@@ -315,6 +319,8 @@ export function decodeStorageRecord(value) {
315
319
  return [value];
316
320
  const tag = value.type;
317
321
  if (tag !== 'text-chunks' && tag !== 'reasoning-chunks' && tag !== 'tool-call-chunks') {
322
+ if (typeof value.seq === 'number')
323
+ SessionSeq(value.seq);
318
324
  return [value];
319
325
  }
320
326
  return expandRow(validateRow(value, tag));