@deepseek-ai/dsh-session 0.0.1-rc.1

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.
@@ -0,0 +1,168 @@
1
+ import { assertNever } from "@deepseek-ai/dsh-llm";
2
+ //#endregion
3
+ //#region lib/types/invariant.js
4
+ /**
5
+ * Package-owned relational invariants for the session event log. Load this
6
+ * companion beside `@deepseek-ai/dsh-invariants` to enable the checks.
7
+ *
8
+ * @module @deepseek-ai/dsh-session/invariant
9
+ */
10
+ const PACKAGE_NAME = "@deepseek-ai/dsh-session";
11
+ /** Cordis companion plugin name. */
12
+ const name = "session-invariant";
13
+ /** Service required before the companion can reserve package ownership. */
14
+ const inject = ["invariants"];
15
+ /** Assert that a step-scoped event names the currently open turn and step. */
16
+ function requireOpenStep(trace, kind, turn, step, fail) {
17
+ if (trace.openTurn !== turn || trace.openStep !== step) fail(`${kind} names turn ${turn}/step ${step} but open is turn ${trace.openTurn}/step ${trace.openStep}`);
18
+ }
19
+ /** Validate one candidate event without mutating the committed trace. */
20
+ function validateEvent(trace, event, fail) {
21
+ if (event.seq <= trace.lastSeq) fail(`seq must strictly increase: saw ${event.seq} after ${trace.lastSeq}`);
22
+ let openTurn = trace.openTurn;
23
+ let openStep = trace.openStep;
24
+ let nextTurn = trace.nextTurn;
25
+ let nextStep = trace.nextStep;
26
+ let pendingCalls = { kind: "none" };
27
+ switch (event.type) {
28
+ case "turn/start":
29
+ if (trace.openTurn !== null) fail(`turn/start ${event.data.turn} while turn ${trace.openTurn} is still open`);
30
+ if (event.data.turn !== trace.nextTurn) fail(`turn/start expected turn ${trace.nextTurn}, got ${event.data.turn}`);
31
+ openTurn = event.data.turn;
32
+ nextStep = 1;
33
+ break;
34
+ case "turn/end":
35
+ if (trace.openTurn !== event.data.turn) fail(`turn/end ${event.data.turn} does not match open turn ${trace.openTurn}`);
36
+ if (trace.openStep !== null) fail(`turn/end ${event.data.turn} while step ${trace.openStep} is still open`);
37
+ openTurn = null;
38
+ nextTurn += 1;
39
+ break;
40
+ case "step/start":
41
+ if (trace.openTurn !== event.data.turn) fail(`step/start in turn ${event.data.turn} but open turn is ${trace.openTurn}`);
42
+ if (trace.openStep !== null) fail(`step/start ${event.data.step} while step ${trace.openStep} is still open`);
43
+ if (event.data.step !== trace.nextStep) fail(`step/start expected step ${trace.nextStep} in turn ${event.data.turn}, got ${event.data.step}`);
44
+ openStep = event.data.step;
45
+ break;
46
+ case "step/end":
47
+ requireOpenStep(trace, "step/end", event.data.turn, event.data.step, fail);
48
+ pendingCalls = { kind: "clear" };
49
+ openStep = null;
50
+ nextStep += 1;
51
+ break;
52
+ case "assistant/chunk":
53
+ requireOpenStep(trace, "assistant/chunk", event.data.turn, event.data.step, fail);
54
+ break;
55
+ case "assistant/message":
56
+ requireOpenStep(trace, "assistant/message", event.data.turn, event.data.step, fail);
57
+ break;
58
+ case "tool/call":
59
+ requireOpenStep(trace, "tool/call", event.data.turn, event.data.step, fail);
60
+ pendingCalls = {
61
+ kind: "add",
62
+ callId: event.data.callId
63
+ };
64
+ break;
65
+ case "tool/result": {
66
+ if (event.surfaceOp !== "append") {
67
+ if (trace.openTurn === null) fail("tool/result surface replacement appended outside any open turn");
68
+ break;
69
+ }
70
+ requireOpenStep(trace, "tool/result", event.data.turn, event.data.step, fail);
71
+ const callId = event.data.message.source.callId;
72
+ const syntheticNotStarted = event.data.message.content[0].isError === true && event.data.error?.code === "TOOL_NOT_STARTED";
73
+ if (!trace.pendingCalls.has(callId) && !syntheticNotStarted) fail(`tool/result for ${callId} with no prior tool/call in this step`);
74
+ pendingCalls = {
75
+ kind: "delete",
76
+ callId
77
+ };
78
+ break;
79
+ }
80
+ case "user/message": break;
81
+ case "session/end-seed": break;
82
+ case "todo/write":
83
+ case "request/header":
84
+ case "request/context":
85
+ if (trace.openTurn === null) fail(`${event.type} appended outside any open turn (core execution events must be turn-enclosed)`);
86
+ break;
87
+ default: break;
88
+ }
89
+ return {
90
+ scalars: {
91
+ lastSeq: event.seq,
92
+ openTurn,
93
+ openStep,
94
+ nextTurn,
95
+ nextStep
96
+ },
97
+ pendingCalls
98
+ };
99
+ }
100
+ /** Apply one already-validated transition after its event commits. */
101
+ function applyTransition(trace, transition) {
102
+ Object.assign(trace, transition.scalars);
103
+ switch (transition.pendingCalls.kind) {
104
+ case "none": break;
105
+ case "add":
106
+ trace.pendingCalls.add(transition.pendingCalls.callId);
107
+ break;
108
+ case "delete":
109
+ trace.pendingCalls.delete(transition.pendingCalls.callId);
110
+ break;
111
+ case "clear":
112
+ trace.pendingCalls.clear();
113
+ break;
114
+ /* v8 ignore next -- validateEvent produces this closed transition union */
115
+ default: assertNever(transition.pendingCalls, "session trace pending-call transition");
116
+ }
117
+ }
118
+ /** Install the session contribution into its child registration fiber. */
119
+ const install = Object.assign((ctx, fail) => {
120
+ const traces = /* @__PURE__ */ new WeakMap();
121
+ const stagedTransitions = /* @__PURE__ */ new WeakMap();
122
+ const freshTrace = () => ({
123
+ lastSeq: -1,
124
+ openTurn: null,
125
+ openStep: null,
126
+ nextTurn: 1,
127
+ nextStep: 1,
128
+ pendingCalls: /* @__PURE__ */ new Set()
129
+ });
130
+ const seedSession = (session) => {
131
+ const trace = freshTrace();
132
+ traces.set(session, trace);
133
+ for (const event of session.events) applyTransition(trace, validateEvent(trace, event, fail));
134
+ return trace;
135
+ };
136
+ /* v8 ignore next -- session/event always follows list() or session/created seeding */
137
+ const traceFor = (session) => traces.get(session) ?? seedSession(session);
138
+ for (const session of ctx.sessions.list()) seedSession(session);
139
+ ctx.on("session/created", (session) => {
140
+ seedSession(session);
141
+ }, { global: true });
142
+ ctx.on("session/event", (session, event) => {
143
+ const staged = stagedTransitions.get(event);
144
+ /* v8 ignore next 2 -- internal/dispatch stages the exact callback arguments */
145
+ if (staged === void 0 || staged.session !== session) return fail("session/event reached publication without matching pre-commit validation");
146
+ stagedTransitions.delete(event);
147
+ applyTransition(staged.trace, staged.transition);
148
+ }, { global: true });
149
+ ctx.on("internal/dispatch", (_mode, eventName, args) => {
150
+ if (eventName !== "session/event") return;
151
+ const [session, event] = args;
152
+ const trace = traceFor(session);
153
+ const transition = validateEvent(trace, event, fail);
154
+ stagedTransitions.set(event, {
155
+ session,
156
+ trace,
157
+ transition
158
+ });
159
+ }, { global: true });
160
+ }, { inject: ["sessions"] });
161
+ /**
162
+ * Register the session invariant companion.
163
+ * @param ctx - Cordis context carrying the invariant service.
164
+ * @returns the installed registration's disposer after setup succeeds.
165
+ */
166
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
167
+ //#endregion
168
+ export { apply, inject, name };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Lossless storage packing for `assistant/chunk` delta runs. Providers stream
3
+ * token-sized deltas, so a log stores hundreds of near-identical event lines
4
+ * whose JSON envelopes dwarf their payloads (~56× measured on a real DeepSeek
5
+ * session). This module packs each run of consecutive same-block delta chunks
6
+ * into ONE storage row — `text-chunks`, `reasoning-chunks`, or
7
+ * `tool-call-chunks` — and expands rows back to the exact original events.
8
+ *
9
+ * Storage rows are a durable-encoding vocabulary, NOT session events: they
10
+ * never enter `Session.events`, have no `SessionEventMap` entry, and use bare
11
+ * (slash-less) type tags so a reader cannot confuse them with the event
12
+ * taxonomy (precedent: the JSONL header line's `session` tag). The encoder
13
+ * whitelists exact shapes — anything it does not fully recognize is stored
14
+ * verbatim, so unknown fields or future chunk variants lose compression, never
15
+ * data. The decoder validates before expanding and fails loud on a malformed
16
+ * row-tagged value instead of silently dropping a whole run.
17
+ *
18
+ * @module @deepseek-ai/dsh-session/chunk-rows
19
+ */
20
+ import { CallId } from '@deepseek-ai/dsh-llm';
21
+ import type { SessionEvent } from './types.ts';
22
+ /**
23
+ * Fields shared by every packed run: placement, block correlation, and member
24
+ * timestamps as gaps. Member `k` reconstructs as seq `seq0 + k` and time
25
+ * `time0` plus the first `k` gaps; a gap may be negative when the wall clock
26
+ * stepped backwards between events.
27
+ */
28
+ interface RunDataBase {
29
+ turn: number;
30
+ step: number;
31
+ /** The stream block index every member shares. */
32
+ index: number;
33
+ /** Epoch-ms gaps between consecutive members; length is one less than the member count. */
34
+ dt: number[];
35
+ }
36
+ /** Payload of a `text-chunks`/`reasoning-chunks` row: one entry per member, never joined — token boundaries are data. */
37
+ interface TextRunData extends RunDataBase {
38
+ texts: string[];
39
+ }
40
+ /** Payload of a `tool-call-chunks` row: the run-constant call identity plus each member's raw arguments fragment. */
41
+ interface ToolCallRunData extends RunDataBase {
42
+ id: CallId;
43
+ /** Present iff every member carried it, with one uniform value (a mixed run never packs). */
44
+ name?: string;
45
+ args: string[];
46
+ }
47
+ /**
48
+ * A packed run of consecutive delta chunk events, discriminated on `type`.
49
+ * `seq0`/`time0` anchor the first member; text and reasoning rows share the
50
+ * {@link TextRunData} payload, tool-call rows carry {@link ToolCallRunData}.
51
+ */
52
+ export type ChunkRow = {
53
+ type: 'text-chunks';
54
+ seq0: number;
55
+ time0: number;
56
+ data: TextRunData;
57
+ } | {
58
+ type: 'reasoning-chunks';
59
+ seq0: number;
60
+ time0: number;
61
+ data: TextRunData;
62
+ } | {
63
+ type: 'tool-call-chunks';
64
+ seq0: number;
65
+ time0: number;
66
+ data: ToolCallRunData;
67
+ };
68
+ /** One durable log line's JSON value: a session event verbatim, or a packed chunk row. */
69
+ export type StorageRecord = SessionEvent | ChunkRow;
70
+ /**
71
+ * Pack an event batch for storage: each run of at least {@link MIN_RUN}
72
+ * consecutive whitelisted same-kind, same-block delta chunk events becomes one
73
+ * {@link ChunkRow}; every other event passes through verbatim, in order.
74
+ * Pure and stateless — safe over any array, including a batch whose runs were
75
+ * split by flush boundaries (the split runs simply pack per batch).
76
+ *
77
+ * @param events - the batch to encode, in log order.
78
+ * @returns the storage records to write, one JSONL line each.
79
+ */
80
+ export declare function packChunkRuns(events: readonly SessionEvent[]): StorageRecord[];
81
+ /**
82
+ * Decode one parsed JSONL line value into the session event(s) it stores.
83
+ * Chunk-row-tagged values validate and expand (a malformed row throws — it is
84
+ * corrupt storage, and treating it as an event would silently drop a whole
85
+ * run); every other value passes through as a single event, unvalidated.
86
+ *
87
+ * @param value - one line's `JSON.parse` result.
88
+ * @returns the stored events, in log order.
89
+ */
90
+ export declare function decodeStorageRecord(value: unknown): SessionEvent[];
91
+ export {};
92
+ //# sourceMappingURL=chunk-rows.d.ts.map
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Lossless storage packing for `assistant/chunk` delta runs. Providers stream
3
+ * token-sized deltas, so a log stores hundreds of near-identical event lines
4
+ * whose JSON envelopes dwarf their payloads (~56× measured on a real DeepSeek
5
+ * session). This module packs each run of consecutive same-block delta chunks
6
+ * into ONE storage row — `text-chunks`, `reasoning-chunks`, or
7
+ * `tool-call-chunks` — and expands rows back to the exact original events.
8
+ *
9
+ * Storage rows are a durable-encoding vocabulary, NOT session events: they
10
+ * never enter `Session.events`, have no `SessionEventMap` entry, and use bare
11
+ * (slash-less) type tags so a reader cannot confuse them with the event
12
+ * taxonomy (precedent: the JSONL header line's `session` tag). The encoder
13
+ * whitelists exact shapes — anything it does not fully recognize is stored
14
+ * verbatim, so unknown fields or future chunk variants lose compression, never
15
+ * data. The decoder validates before expanding and fails loud on a malformed
16
+ * row-tagged value instead of silently dropping a whole run.
17
+ *
18
+ * @module @deepseek-ai/dsh-session/chunk-rows
19
+ */
20
+ import { CallId, assertNever } from '@deepseek-ai/dsh-llm';
21
+ /**
22
+ * Minimum members before a run packs. Below it a row's envelope rivals the
23
+ * event lines it replaces. A format constant, not a tunable: both layouts
24
+ * decode identically, so changing it never invalidates stored logs.
25
+ */
26
+ const MIN_RUN = 3;
27
+ function isRecord(value) {
28
+ return typeof value === 'object' && value !== null;
29
+ }
30
+ /** Exact-key check: `value` has every key in `keys` and nothing else. */
31
+ function hasExactKeys(value, keys) {
32
+ return Object.keys(value).length === keys.length && keys.every(k => Object.hasOwn(value, k));
33
+ }
34
+ /**
35
+ * Classify an event for packing: its delta kind when the ENTIRE shape
36
+ * (envelope, data, chunk — exact keys, primitive types, integer seq/time) is
37
+ * whitelisted, else `undefined` (store verbatim). Inputs come from live typed
38
+ * appends AND parsed fixture files, so the checks are structural, not
39
+ * type-trusted. Integer times keep gap encoding exact: a fractional time would
40
+ * reconstruct through float subtraction/addition, which need not round-trip.
41
+ */
42
+ function classify(event) {
43
+ if (event.type !== 'assistant/chunk')
44
+ return undefined;
45
+ if (!hasExactKeys(event, ['type', 'seq', 'time', 'data']))
46
+ return undefined;
47
+ if (!Number.isSafeInteger(event.seq) || event.seq < 0 || !Number.isSafeInteger(event.time))
48
+ return undefined;
49
+ const data = event.data;
50
+ if (!isRecord(data) || !hasExactKeys(data, ['turn', 'step', 'chunk']))
51
+ return undefined;
52
+ if (typeof data.turn !== 'number' || typeof data.step !== 'number')
53
+ return undefined;
54
+ const chunk = data.chunk;
55
+ if (!isRecord(chunk) || typeof chunk.index !== 'number')
56
+ return undefined;
57
+ switch (chunk.type) {
58
+ case 'text-delta':
59
+ case 'reasoning-delta':
60
+ return hasExactKeys(chunk, ['type', 'index', 'text']) && typeof chunk.text === 'string'
61
+ ? chunk.type
62
+ : undefined;
63
+ case 'tool-call-delta': {
64
+ const shapeOk = hasExactKeys(chunk, ['type', 'index', 'id', 'argumentsDelta'])
65
+ || (hasExactKeys(chunk, ['type', 'index', 'id', 'name', 'argumentsDelta']) && typeof chunk.name === 'string');
66
+ return shapeOk && typeof chunk.id === 'string' && typeof chunk.argumentsDelta === 'string'
67
+ ? chunk.type
68
+ : undefined;
69
+ }
70
+ // Whitelist fall-through over parsed data: block-start/end, usage, finish,
71
+ // and any future chunk variant stay one event per line.
72
+ default:
73
+ return undefined;
74
+ }
75
+ }
76
+ /** The tool-call fields of a whitelisted delta chunk (only after {@link classify} returned `'tool-call-delta'`). */
77
+ function toolCallOf(event) {
78
+ return event.data.chunk;
79
+ }
80
+ /** The block index of a whitelisted delta chunk (not every {@link StreamChunk} variant carries one). */
81
+ function indexOf(event) {
82
+ return event.data.chunk.index;
83
+ }
84
+ /** Whether `next` extends a run ending in `prev` (same kind already checked by the caller). */
85
+ function continues(prev, next, kind) {
86
+ if (next.seq !== prev.seq + 1)
87
+ return false;
88
+ // Two safe-integer times can sit further apart than a double subtracts
89
+ // exactly (2^53-1 and its negation differ by ~2^54); a rounded gap would
90
+ // decode to a different timestamp. The check is exact in both directions: a
91
+ // true gap within safe range subtracts without rounding and passes, while a
92
+ // true gap beyond it rounds to a value that is itself beyond and fails.
93
+ if (!Number.isSafeInteger(next.time - prev.time))
94
+ return false;
95
+ if (next.data.turn !== prev.data.turn || next.data.step !== prev.data.step)
96
+ return false;
97
+ if (indexOf(next) !== indexOf(prev))
98
+ return false;
99
+ if (kind !== 'tool-call-delta')
100
+ return true;
101
+ const a = toolCallOf(prev);
102
+ const b = toolCallOf(next);
103
+ // `name` must match in presence AND value — a mixed run is not representable.
104
+ return a.id === b.id && Object.hasOwn(a, 'name') === Object.hasOwn(b, 'name') && a.name === b.name;
105
+ }
106
+ /** Build the row for a completed run (`run.length >= MIN_RUN`, uniform per {@link continues}). */
107
+ function buildRow(kind, run) {
108
+ const first = run[0];
109
+ const base = {
110
+ turn: first.data.turn,
111
+ step: first.data.step,
112
+ index: indexOf(first),
113
+ dt: run.slice(1).map((event, i) => event.time - run[i].time),
114
+ };
115
+ const envelope = { seq0: first.seq, time0: first.time };
116
+ if (kind === 'tool-call-delta') {
117
+ const call = toolCallOf(first);
118
+ return {
119
+ type: 'tool-call-chunks',
120
+ ...envelope,
121
+ data: {
122
+ ...base,
123
+ id: CallId(call.id),
124
+ ...Object.hasOwn(call, 'name') ? { name: call.name } : {},
125
+ args: run.map(event => event.data.chunk.argumentsDelta),
126
+ },
127
+ };
128
+ }
129
+ const data = { ...base, texts: run.map(event => event.data.chunk.text) };
130
+ return kind === 'text-delta'
131
+ ? { type: 'text-chunks', ...envelope, data }
132
+ : { type: 'reasoning-chunks', ...envelope, data };
133
+ }
134
+ /**
135
+ * Pack an event batch for storage: each run of at least {@link MIN_RUN}
136
+ * consecutive whitelisted same-kind, same-block delta chunk events becomes one
137
+ * {@link ChunkRow}; every other event passes through verbatim, in order.
138
+ * Pure and stateless — safe over any array, including a batch whose runs were
139
+ * split by flush boundaries (the split runs simply pack per batch).
140
+ *
141
+ * @param events - the batch to encode, in log order.
142
+ * @returns the storage records to write, one JSONL line each.
143
+ */
144
+ export function packChunkRuns(events) {
145
+ const out = [];
146
+ let kind;
147
+ let run = [];
148
+ const flush = () => {
149
+ if (kind !== undefined && run.length >= MIN_RUN)
150
+ out.push(buildRow(kind, run));
151
+ else
152
+ out.push(...run);
153
+ kind = undefined;
154
+ run = [];
155
+ };
156
+ for (const event of events) {
157
+ const k = classify(event);
158
+ if (k === undefined) {
159
+ flush();
160
+ out.push(event);
161
+ continue;
162
+ }
163
+ const delta = event;
164
+ const last = run[run.length - 1];
165
+ if (k === kind && last !== undefined && continues(last, delta, k)) {
166
+ run.push(delta);
167
+ continue;
168
+ }
169
+ flush();
170
+ kind = k;
171
+ run = [delta];
172
+ }
173
+ flush();
174
+ return out;
175
+ }
176
+ /** Throw the uniform malformed-row diagnostic. */
177
+ function malformed(tag, why) {
178
+ throw new Error(`malformed ${tag} storage row: ${why}`);
179
+ }
180
+ /** Validate the shared run-data fields and the payload/dt arity; returns the member payload. */
181
+ function validateRunData(tag, data, payloadKey) {
182
+ if (typeof data.turn !== 'number' || typeof data.step !== 'number' || typeof data.index !== 'number') {
183
+ malformed(tag, 'turn/step/index must be numbers');
184
+ }
185
+ const payload = data[payloadKey];
186
+ if (!Array.isArray(payload) || payload.length === 0 || payload.some(entry => typeof entry !== 'string')) {
187
+ malformed(tag, `${payloadKey} must be a non-empty string array`);
188
+ }
189
+ const dt = data.dt;
190
+ if (!Array.isArray(dt) || dt.some(gap => !Number.isSafeInteger(gap))) {
191
+ malformed(tag, 'dt must be an array of safe integers');
192
+ }
193
+ if (dt.length !== payload.length - 1) {
194
+ malformed(tag, `dt length ${dt.length} does not match ${payload.length} members`);
195
+ }
196
+ return payload;
197
+ }
198
+ /** Validate a row-tagged parsed value's envelope and data, throwing on any malformation. */
199
+ function validateRow(value, tag) {
200
+ if (!hasExactKeys(value, ['type', 'seq0', 'time0', 'data'])) {
201
+ malformed(tag, 'envelope must be exactly {type, seq0, time0, data}');
202
+ }
203
+ if (!Number.isSafeInteger(value.seq0) || value.seq0 < 0) {
204
+ malformed(tag, 'seq0 must be a non-negative safe integer');
205
+ }
206
+ if (!Number.isSafeInteger(value.time0)) {
207
+ malformed(tag, 'time0 must be a safe integer');
208
+ }
209
+ const data = value.data;
210
+ if (!isRecord(data))
211
+ malformed(tag, 'data must be an object');
212
+ let payload;
213
+ if (tag === 'tool-call-chunks') {
214
+ const withName = hasExactKeys(data, ['turn', 'step', 'index', 'id', 'name', 'dt', 'args']);
215
+ if (!withName && !hasExactKeys(data, ['turn', 'step', 'index', 'id', 'dt', 'args'])) {
216
+ malformed(tag, 'data must be exactly {turn, step, index, id, name?, dt, args}');
217
+ }
218
+ if (typeof data.id !== 'string' || (withName && typeof data.name !== 'string')) {
219
+ malformed(tag, 'id (and name when present) must be strings');
220
+ }
221
+ payload = validateRunData(tag, data, 'args');
222
+ }
223
+ else {
224
+ if (!hasExactKeys(data, ['turn', 'step', 'index', 'dt', 'texts'])) {
225
+ malformed(tag, 'data must be exactly {turn, step, index, dt, texts}');
226
+ }
227
+ payload = validateRunData(tag, data, 'texts');
228
+ }
229
+ // Reconstruction bounds. The encoder only packs runs whose member seqs and
230
+ // times are all safe integers, so a running value that leaves safe range is
231
+ // outside any encoder's image: float arithmetic would round it to a
232
+ // different number than exact arithmetic, a silent corruption. Within safe
233
+ // range every step is exact, so the first departure is always caught.
234
+ if (!Number.isSafeInteger(value.seq0 + payload.length - 1)) {
235
+ malformed(tag, 'member seqs must stay safe integers');
236
+ }
237
+ let time = value.time0;
238
+ for (const gap of data.dt) {
239
+ time += gap;
240
+ if (!Number.isSafeInteger(time))
241
+ malformed(tag, 'member times must stay safe integers');
242
+ }
243
+ return value;
244
+ }
245
+ /** Expand a validated row back into its exact original events, in order. */
246
+ function expandRow(row) {
247
+ const members = row.type === 'tool-call-chunks' ? row.data.args : row.data.texts;
248
+ const events = [];
249
+ let time = row.time0;
250
+ for (let k = 0; k < members.length; k++) {
251
+ if (k > 0)
252
+ time += row.data.dt[k - 1];
253
+ let chunk;
254
+ switch (row.type) {
255
+ case 'text-chunks':
256
+ chunk = { type: 'text-delta', index: row.data.index, text: members[k] };
257
+ break;
258
+ case 'reasoning-chunks':
259
+ chunk = { type: 'reasoning-delta', index: row.data.index, text: members[k] };
260
+ break;
261
+ case 'tool-call-chunks':
262
+ chunk = {
263
+ type: 'tool-call-delta',
264
+ index: row.data.index,
265
+ id: row.data.id,
266
+ ...Object.hasOwn(row.data, 'name') ? { name: row.data.name } : {},
267
+ argumentsDelta: members[k],
268
+ };
269
+ break;
270
+ /* v8 ignore next 2 -- validateRow only returns the three row tags */
271
+ default:
272
+ return assertNever(row, 'chunk-rows expandRow');
273
+ }
274
+ events.push({
275
+ type: 'assistant/chunk',
276
+ seq: row.seq0 + k,
277
+ time,
278
+ data: { turn: row.data.turn, step: row.data.step, chunk },
279
+ });
280
+ }
281
+ return events;
282
+ }
283
+ /**
284
+ * Decode one parsed JSONL line value into the session event(s) it stores.
285
+ * Chunk-row-tagged values validate and expand (a malformed row throws — it is
286
+ * corrupt storage, and treating it as an event would silently drop a whole
287
+ * run); every other value passes through as a single event, unvalidated.
288
+ *
289
+ * @param value - one line's `JSON.parse` result.
290
+ * @returns the stored events, in log order.
291
+ */
292
+ export function decodeStorageRecord(value) {
293
+ if (!isRecord(value))
294
+ return [value];
295
+ const tag = value.type;
296
+ if (tag !== 'text-chunks' && tag !== 'reasoning-chunks' && tag !== 'tool-call-chunks') {
297
+ return [value];
298
+ }
299
+ return expandRow(validateRow(value, tag));
300
+ }
301
+ //# sourceMappingURL=chunk-rows.js.map