@deepseek-ai/dsh-session 0.1.2-alpha.3 → 0.1.2-alpha.5
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 +9 -1
- package/README.zh.md +9 -1
- package/lib/index.js +119 -55
- package/lib/invariant.js +1 -1
- package/lib/types/chunk-rows.d.ts +7 -6
- package/lib/types/chunk-rows.js +11 -5
- package/lib/types/index.d.ts +40 -16
- package/lib/types/index.js +87 -38
- package/lib/types/invariant.js +1 -1
- package/lib/types/repair.js +4 -3
- package/lib/types/seq-ranges.d.ts +3 -2
- package/lib/types/seq-ranges.js +3 -2
- package/lib/types/surface.d.ts +9 -8
- package/lib/types/surface.js +11 -7
- package/lib/types/types.d.ts +39 -12
- package/lib/types/types.js +23 -1
- package/package.json +9 -9
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:
|
|
6
|
-
README.zh.md:
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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")
|
|
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 (
|
|
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
|
-
*
|
|
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
|
|
1231
|
-
*
|
|
1232
|
-
* while
|
|
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
|
|
1324
|
+
/** Cached immutable full snapshot of the private append-only log. */
|
|
1290
1325
|
eventsSnapshot;
|
|
1291
1326
|
/**
|
|
1292
|
-
*
|
|
1293
|
-
*
|
|
1294
|
-
*
|
|
1295
|
-
|
|
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
|
-
|
|
1298
|
-
this.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
1813
|
+
isSeeded: true
|
|
1750
1814
|
}
|
|
1751
1815
|
});
|
|
1752
1816
|
}
|
|
1753
1817
|
_forkSeed(session, requestedBoundary) {
|
|
1754
|
-
const
|
|
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 >=
|
|
1764
|
-
const lastSeq =
|
|
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 =
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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:
|
|
55
|
+
seq0: SessionSeqType;
|
|
56
56
|
time0: number;
|
|
57
57
|
data: TextRunData;
|
|
58
58
|
} | {
|
|
59
59
|
type: 'reasoning-chunks';
|
|
60
|
-
seq0:
|
|
60
|
+
seq0: SessionSeqType;
|
|
61
61
|
time0: number;
|
|
62
62
|
data: TextRunData;
|
|
63
63
|
} | {
|
|
64
64
|
type: 'tool-call-chunks';
|
|
65
|
-
seq0:
|
|
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
|
|
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.
|
package/lib/types/chunk-rows.js
CHANGED
|
@@ -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.
|
|
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 ||
|
|
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
|
|
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));
|