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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,15 +1,17 @@
1
1
  import { Service } from "@deepseek-ai/cordis";
2
2
  import { isAbsolute } from "node:path";
3
- import { CallId, MessageId, assertNever, callConfigEquals, deepFreeze, freezeMessage } from "@deepseek-ai/dsh-llm";
3
+ import { brandString } from "@deepseek-ai/dsh-brand";
4
+ import { deepFreeze, snapshotJsonValue } from "@deepseek-ai/dsh-util-values";
4
5
  import { scopeOf, scopeTarget } from "@deepseek-ai/dsh-scope";
6
+ import { callConfigEquals } from "@deepseek-ai/dsh-llm";
5
7
  //#region lib/types/types.js
6
8
  /**
7
9
  * Brand a string as a {@link SessionId}.
8
10
  * @param id - the raw session id string.
9
- * @returns the same string, branded (a compile-time cast — no runtime cost).
11
+ * @returns the same string with the session-id brand.
10
12
  */
11
13
  function SessionId(id) {
12
- return id;
14
+ return brandString(id);
13
15
  }
14
16
  /**
15
17
  * The on-disk session format version, stamped into every newly-written {@link SessionHeader}
@@ -36,175 +38,6 @@ function SessionId(id) {
36
38
  */
37
39
  const SESSION_FORMAT_VERSION = 0;
38
40
  //#endregion
39
- //#region lib/types/json.js
40
- /** Lossless-JSON validation and detached snapshots for durable session data. @module @deepseek-ai/dsh-session/json */
41
- /** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
42
- function hasIntrinsicConstructor(prototype, name) {
43
- const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor")?.value;
44
- if (typeof constructor !== "function") return false;
45
- try {
46
- return constructor.name === name && constructor.prototype === prototype && Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`;
47
- } catch {
48
- return false;
49
- }
50
- }
51
- /** Whether a candidate is one realm's intrinsic `Object.prototype`. */
52
- function isIntrinsicObjectPrototype(value) {
53
- return Object.getPrototypeOf(value) === null && hasIntrinsicConstructor(value, "Object");
54
- }
55
- /** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */
56
- function hasPlainArrayPrototype(value) {
57
- const prototype = Object.getPrototypeOf(value);
58
- if (!Array.isArray(prototype) || !hasIntrinsicConstructor(prototype, "Array")) return false;
59
- const objectPrototype = Object.getPrototypeOf(prototype);
60
- return typeof objectPrototype === "object" && objectPrototype !== null && isIntrinsicObjectPrototype(objectPrototype);
61
- }
62
- /** Whether an object is a plain or null-prototype record from any JavaScript realm. */
63
- function hasPlainObjectPrototype(value) {
64
- const prototype = Object.getPrototypeOf(value);
65
- return prototype === null || typeof prototype === "object" && isIntrinsicObjectPrototype(prototype);
66
- }
67
- /** Return every JSON-visible object key, or reject own data JSON would discard. */
68
- function enumerableStringKeys(value) {
69
- const keys = Reflect.ownKeys(value);
70
- if (keys.some((key) => typeof key !== "string" || !Object.prototype.propertyIsEnumerable.call(value, key))) return void 0;
71
- return keys;
72
- }
73
- /** Validate lossless JSON iteratively, optionally materializing a detached snapshot. */
74
- function walkJsonValue(value, detach) {
75
- const ancestors = /* @__PURE__ */ new Set();
76
- let root;
77
- const assign = (destination, item) => {
78
- if (destination === void 0) return;
79
- if (destination.kind === "root") root = item;
80
- else if (destination.kind === "array") destination.target[destination.index] = item;
81
- else Object.defineProperty(destination.target, destination.key, {
82
- value: item,
83
- enumerable: true,
84
- configurable: true,
85
- writable: true
86
- });
87
- };
88
- const tasks = [{
89
- kind: "visit",
90
- value,
91
- ...detach ? { destination: { kind: "root" } } : {}
92
- }];
93
- for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
94
- if (task.kind === "leave") {
95
- ancestors.delete(task.source);
96
- continue;
97
- }
98
- if (task.kind === "array-item") {
99
- if (!Object.prototype.hasOwnProperty.call(task.source, task.index)) return void 0;
100
- tasks.push({
101
- kind: "visit",
102
- value: task.source[task.index],
103
- ...task.target === void 0 ? {} : { destination: {
104
- kind: "array",
105
- target: task.target,
106
- index: task.index
107
- } }
108
- });
109
- continue;
110
- }
111
- if (task.kind === "object-property") {
112
- tasks.push({
113
- kind: "visit",
114
- value: task.source[task.key],
115
- ...task.target === void 0 ? {} : { destination: {
116
- kind: "object",
117
- target: task.target,
118
- key: task.key
119
- } }
120
- });
121
- continue;
122
- }
123
- const current = task.value;
124
- if (current === null) {
125
- assign(task.destination, null);
126
- continue;
127
- }
128
- if (typeof current === "boolean" || typeof current === "string") {
129
- assign(task.destination, current);
130
- continue;
131
- }
132
- if (typeof current === "number") {
133
- if (!Number.isFinite(current) || Object.is(current, -0)) return void 0;
134
- assign(task.destination, current);
135
- continue;
136
- }
137
- if (typeof current !== "object") return void 0;
138
- if (ancestors.has(current)) return void 0;
139
- if (Array.isArray(current)) {
140
- if (!hasPlainArrayPrototype(current)) return void 0;
141
- const length = current.length;
142
- if (Reflect.ownKeys(current).length !== length + 1) return void 0;
143
- const target = detach ? [] : void 0;
144
- if (target !== void 0) assign(task.destination, target);
145
- ancestors.add(current);
146
- tasks.push({
147
- kind: "leave",
148
- source: current
149
- });
150
- for (let index = length - 1; index >= 0; index--) tasks.push({
151
- kind: "array-item",
152
- source: current,
153
- index,
154
- ...target === void 0 ? {} : { target }
155
- });
156
- continue;
157
- }
158
- if (!hasPlainObjectPrototype(current)) return void 0;
159
- const keys = enumerableStringKeys(current);
160
- if (keys === void 0) return void 0;
161
- const target = detach ? {} : void 0;
162
- if (target !== void 0) assign(task.destination, target);
163
- ancestors.add(current);
164
- tasks.push({
165
- kind: "leave",
166
- source: current
167
- });
168
- for (let index = keys.length - 1; index >= 0; index--) {
169
- const key = keys[index];
170
- /* v8 ignore next -- the loop is bounded by the captured key count. */
171
- if (key === void 0) return void 0;
172
- tasks.push({
173
- kind: "object-property",
174
- source: current,
175
- key,
176
- ...target === void 0 ? {} : { target }
177
- });
178
- }
179
- }
180
- return detach ? root : true;
181
- }
182
- /**
183
- * Validate and detach lossless JSON in one read per property, so a stateful
184
- * getter cannot change between validation and copying. Traversal is iterative,
185
- * so valid nesting is bounded by available memory rather than the JavaScript
186
- * call stack. Accepts ordinary arrays, plain or null-prototype objects, and JSON
187
- * scalars; rejects sparse, cyclic, exotic, negative-zero, and non-finite values.
188
- * Getter throws propagate.
189
- *
190
- * @param value - the candidate value to validate and detach.
191
- * @returns the detached snapshot, or `undefined` when the value is not
192
- * losslessly JSON-serializable.
193
- */
194
- function snapshotJsonValue(value) {
195
- return walkJsonValue(value, true);
196
- }
197
- /**
198
- * Test the same lossless JSON boundary as {@link snapshotJsonValue} without
199
- * detaching it. Only own enumerable string properties participate; `toJSON`
200
- * is ignored and getters run, so persistence boundaries use the snapshotter.
201
- * @param value - the candidate event data to test.
202
- * @returns whether `value` survives JSON round-trip losslessly.
203
- */
204
- function isJsonValue(value) {
205
- return walkJsonValue(value, false) === true;
206
- }
207
- //#endregion
208
41
  //#region lib/types/surface.js
209
42
  /**
210
43
  * Surface layer on top of the session event log: an ordered view of events
@@ -666,8 +499,8 @@ function interruptedTurnClosers(events) {
666
499
  const closers = [];
667
500
  for (const [callId, { step, callSeq }] of pendingCalls) {
668
501
  const started = callSeq !== void 0;
669
- const message = freezeMessage({
670
- id: MessageId(`interrupted-tool-result-${callId}-${seq}`),
502
+ const message = deepFreeze({
503
+ id: brandString(`interrupted-tool-result-${callId}-${seq}`),
671
504
  role: "user",
672
505
  source: {
673
506
  kind: "tool",
@@ -726,21 +559,22 @@ function interruptedTurnClosers(events) {
726
559
  //#endregion
727
560
  //#region lib/types/chunk-rows.js
728
561
  /**
729
- * Lossless storage packing for `assistant/chunk` delta runs. Providers stream
562
+ * Lossless row packing for `assistant/chunk` delta runs. Providers stream
730
563
  * token-sized deltas, so a log stores hundreds of near-identical event lines
731
564
  * whose JSON envelopes dwarf their payloads (~56× measured on a real DeepSeek
732
565
  * session). This module packs each run of consecutive same-block delta chunks
733
566
  * into ONE storage row — `text-chunks`, `reasoning-chunks`, or
734
567
  * `tool-call-chunks` — and expands rows back to the exact original events.
735
568
  *
736
- * Storage rows are a durable-encoding vocabulary, NOT session events: they
737
- * never enter `Session.events`, have no `SessionEventMap` entry, and use bare
738
- * (slash-less) type tags so a reader cannot confuse them with the event
739
- * taxonomy (precedent: the JSONL header line's `session` tag). The encoder
740
- * whitelists exact shapes anything it does not fully recognize is stored
741
- * verbatim, so unknown fields or future chunk variants lose compression, never
742
- * data. The decoder validates before expanding and fails loud on a malformed
743
- * row-tagged value instead of silently dropping a whole run.
569
+ * Packed rows are an encoding vocabulary, NOT session events: they never enter
570
+ * `Session.events`, have no `SessionEventMap` entry, and use bare (slash-less)
571
+ * type tags so a reader cannot confuse them with the event taxonomy
572
+ * (precedent: the JSONL header line's `session` tag). Persistence and bounded
573
+ * history transport both use the codec. The encoder whitelists exact shapes
574
+ * anything it does not fully recognize stays verbatim, so unknown fields or
575
+ * future chunk variants lose compression, never data. The decoder validates
576
+ * before expanding and fails loud on a malformed row-tagged value instead of
577
+ * silently dropping a whole run.
744
578
  *
745
579
  * @module @deepseek-ai/dsh-session/chunk-rows
746
580
  */
@@ -844,7 +678,7 @@ function buildRow(kind, run) {
844
678
  ...envelope,
845
679
  data: {
846
680
  ...base,
847
- id: CallId(call.id),
681
+ id: brandString(call.id),
848
682
  ...Object.hasOwn(call, "name") ? { name: call.name } : {},
849
683
  args: run.map((event) => event.data.chunk.argumentsDelta)
850
684
  }
@@ -961,7 +795,7 @@ function validateRow(value, tag) {
961
795
  ])) malformed(tag, "data must be exactly {turn, step, index, dt, texts}");
962
796
  payload = validateRunData(tag, data, "texts");
963
797
  }
964
- if (!Number.isSafeInteger(value.seq0 + payload.length - 1)) malformed(tag, "member seqs must stay safe integers");
798
+ if (payload.length - 1 > Number.MAX_SAFE_INTEGER - value.seq0) malformed(tag, "member seqs must stay safe integers");
965
799
  let time = value.time0;
966
800
  for (const gap of data.dt) {
967
801
  time += gap;
@@ -1001,8 +835,8 @@ function expandRow(row) {
1001
835
  argumentsDelta: members[k]
1002
836
  };
1003
837
  break;
1004
- /* v8 ignore next 2 -- validateRow only returns the three row tags */
1005
- default: return assertNever(row, "chunk-rows expandRow");
838
+ /* v8 ignore next 4 -- validateRow only returns the three row tags */
839
+ default: throw new Error(`chunk-rows received unsupported row ${String(row)}`);
1006
840
  }
1007
841
  events.push({
1008
842
  type: "assistant/chunk",
@@ -1048,8 +882,11 @@ function decodeStorageRecord(value) {
1048
882
  * in `./types.ts`): such a log was likely written by a newer harness, and
1049
883
  * silently skipping a required event would reconstruct a wrong session.
1050
884
  * Downstream (out-of-repo) plugin events are outside this list by
1051
- * construction; a registration surface for them is deferred until such a
1052
- * consumer exists.
885
+ * construction. The persisted `SessionEvent.ignorable` marker is the
886
+ * compatibility mechanism; event-name registration was rejected because
887
+ * it does not classify omission safety and would make reads
888
+ * composition-dependent. The rationale is in
889
+ * `.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.md`.
1053
890
  */
1054
891
  const KNOWN_SESSION_EVENT_TYPES = new Set([
1055
892
  "agent-preset/selected",
@@ -1071,18 +908,21 @@ const KNOWN_SESSION_EVENT_TYPES = new Set([
1071
908
  "hook/result",
1072
909
  "llm/retry",
1073
910
  "llm/retry-started",
911
+ "model/selection",
1074
912
  "permission/preset",
1075
913
  "plan/mode",
1076
914
  "request/context",
1077
915
  "request/header",
1078
916
  "sandbox/mode",
1079
917
  "schedule/change",
918
+ "session-log-deepseek/delivery-accepted",
1080
919
  "session/end-seed",
1081
920
  "session/title",
1082
921
  "session/title-llm-request",
1083
922
  "step/end",
1084
923
  "step/start",
1085
924
  "subagent/descriptor",
925
+ "subagent/model-selection-policy",
1086
926
  "team/member",
1087
927
  "team/message/delivered",
1088
928
  "team/message/queued",
@@ -1102,6 +942,62 @@ const KNOWN_SESSION_EVENT_TYPES = new Set([
1102
942
  "web/deepseek-search-llm-request"
1103
943
  ]);
1104
944
  //#endregion
945
+ //#region lib/types/seq-ranges.js
946
+ /** Lossless range encoding for JSONL `sourceEventSeqs` arrays. */
947
+ function isStrictlyIncreasing(values) {
948
+ return values.every((value, index) => index === 0 || value > values[index - 1]);
949
+ }
950
+ /**
951
+ * Replace profitable consecutive runs with inclusive pairs.
952
+ * @param values - validated in-memory source sequences.
953
+ * @returns a lossless JSON storage form.
954
+ */
955
+ function encodeSeqRanges(values) {
956
+ if (!isStrictlyIncreasing(values)) return [...values];
957
+ const encoded = [];
958
+ for (let start = 0; start < values.length;) {
959
+ let end = start;
960
+ while (end + 1 < values.length && values[end + 1] === values[end] + 1) end += 1;
961
+ if (end - start >= 2) encoded.push([values[start], values[end]]);
962
+ else for (let index = start; index <= end; index += 1) encoded.push(values[index]);
963
+ start = end + 1;
964
+ }
965
+ return encoded;
966
+ }
967
+ /**
968
+ * Expand a JSON storage-form source sequence array.
969
+ * @param value - parsed storage value.
970
+ * @param maxEntries - largest list permitted by the owning event.
971
+ * @returns the in-memory source sequences.
972
+ */
973
+ function decodeSeqRanges(value, maxEntries = Number.MAX_SAFE_INTEGER) {
974
+ if (!Array.isArray(value)) throw new TypeError("sourceEventSeqs must be an array");
975
+ const decoded = [];
976
+ let hasRange = false;
977
+ for (const entry of value) {
978
+ if (typeof entry === "number") {
979
+ assertSeq(entry);
980
+ if (decoded.length >= maxEntries) throw new TypeError("sourceEventSeqs exceeds its event sequence");
981
+ decoded.push(entry);
982
+ continue;
983
+ }
984
+ if (!Array.isArray(entry) || entry.length !== 2) throw new TypeError("sourceEventSeqs range entries must be [start, end] pairs");
985
+ const start = entry[0];
986
+ const end = entry[1];
987
+ assertSeq(start);
988
+ assertSeq(end);
989
+ if (end < start) throw new TypeError("sourceEventSeqs ranges require start <= end");
990
+ 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);
992
+ hasRange = true;
993
+ }
994
+ if (hasRange && !isStrictlyIncreasing(decoded)) throw new TypeError("sourceEventSeqs ranges must be strictly increasing");
995
+ return decoded;
996
+ }
997
+ function assertSeq(value) {
998
+ if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("sourceEventSeqs must contain non-negative safe integers");
999
+ }
1000
+ //#endregion
1105
1001
  //#region lib/types/index.js
1106
1002
  /**
1107
1003
  * Event-sourced session service: append-only session log, in-memory store, and
@@ -1433,7 +1329,7 @@ var Session = class Session {
1433
1329
  * Map/Set/Date/class instance), or when the candidate violates the
1434
1330
  * canonical surface contract (marker shape and eligibility, unique
1435
1331
  * earlier source-event references, positional replacement validity, and complete
1436
- * shadowed-node coverage). One recursive pass reads, validates, and
1332
+ * shadowed-node coverage). One iterative pass reads, validates, and
1437
1333
  * copies each nested value once, so a stateful getter cannot supply one value
1438
1334
  * to validation and another to storage. The event log is the durable source
1439
1335
  * of truth, so a bad event fails at the append site rather than later during
@@ -1648,9 +1544,9 @@ var SessionStore = class extends Service {
1648
1544
  prepare(id, options) {
1649
1545
  let sessionId;
1650
1546
  if (id === void 0) do
1651
- sessionId = SessionId(`session-${++this.counter}`);
1547
+ sessionId = brandString(`session-${++this.counter}`);
1652
1548
  while (this.store.has(sessionId));
1653
- else sessionId = SessionId(id);
1549
+ else sessionId = brandString(id);
1654
1550
  if (this.store.has(sessionId)) throw new Error(`session "${sessionId}" already exists`);
1655
1551
  if (options?.seedSource === "persistence") return Session.fromRestore(sessionId, options.seed, options.meta);
1656
1552
  const seed = options?.seed;
@@ -1887,4 +1783,4 @@ var SessionStore = class extends Service {
1887
1783
  }
1888
1784
  };
1889
1785
  //#endregion
1890
- export { KNOWN_SESSION_EVENT_TYPES, SESSION_FORMAT_VERSION, Session, SessionForkError, SessionId, SessionPreparation, SessionStore, SessionStore as default, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN, adoptSessionEvent, canonicalHeader, decodeStorageRecord, deriveEventMessage, foldRequestHeader, foldSurface, headerEquals, interruptedTurnClosers, isAppendSurfaceEvent, isJsonValue, isReplacementSurfaceEvent, isSurfaceEligibleType, isSurfaceEvent, packChunkRuns, snapshotJsonValue, snapshotSessionEvent };
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 };
package/lib/invariant.js CHANGED
@@ -1,4 +1,5 @@
1
- import { assertNever } from "@deepseek-ai/dsh-llm";
1
+ import { assertNever } from "@deepseek-ai/dsh-util-values";
2
+ import "@deepseek-ai/dsh-brand";
2
3
  //#endregion
3
4
  //#region lib/types/invariant.js
4
5
  /**
@@ -79,7 +80,6 @@ function validateEvent(trace, event, fail) {
79
80
  }
80
81
  case "user/message": break;
81
82
  case "session/end-seed": break;
82
- case "todo/write":
83
83
  case "request/header":
84
84
  case "request/context":
85
85
  if (trace.openTurn === null) fail(`${event.type} appended outside any open turn (core execution events must be turn-enclosed)`);
@@ -1,23 +1,24 @@
1
1
  /**
2
- * Lossless storage packing for `assistant/chunk` delta runs. Providers stream
2
+ * Lossless row packing for `assistant/chunk` delta runs. Providers stream
3
3
  * token-sized deltas, so a log stores hundreds of near-identical event lines
4
4
  * whose JSON envelopes dwarf their payloads (~56× measured on a real DeepSeek
5
5
  * session). This module packs each run of consecutive same-block delta chunks
6
6
  * into ONE storage row — `text-chunks`, `reasoning-chunks`, or
7
7
  * `tool-call-chunks` — and expands rows back to the exact original events.
8
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.
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)
11
+ * type tags so a reader cannot confuse them with the event taxonomy
12
+ * (precedent: the JSONL header line's `session` tag). Persistence and bounded
13
+ * history transport both use the codec. The encoder whitelists exact shapes
14
+ * anything it does not fully recognize stays verbatim, so unknown fields or
15
+ * future chunk variants lose compression, never data. The decoder validates
16
+ * before expanding and fails loud on a malformed row-tagged value instead of
17
+ * silently dropping a whole run.
17
18
  *
18
19
  * @module @deepseek-ai/dsh-session/chunk-rows
19
20
  */
20
- import { CallId } from '@deepseek-ai/dsh-llm';
21
+ import type { ToolCallId } from '@deepseek-ai/dsh-llm/brand';
21
22
  import type { SessionEvent } from './types.ts';
22
23
  /**
23
24
  * Fields shared by every packed run: placement, block correlation, and member
@@ -39,7 +40,7 @@ interface TextRunData extends RunDataBase {
39
40
  }
40
41
  /** Payload of a `tool-call-chunks` row: the run-constant call identity plus each member's raw arguments fragment. */
41
42
  interface ToolCallRunData extends RunDataBase {
42
- id: CallId;
43
+ id: ToolCallId;
43
44
  /** Present iff every member carried it, with one uniform value (a mixed run never packs). */
44
45
  name?: string;
45
46
  args: string[];
@@ -67,6 +68,18 @@ export type ChunkRow = {
67
68
  };
68
69
  /** One durable log line's JSON value: a session event verbatim, or a packed chunk row. */
69
70
  export type StorageRecord = SessionEvent | ChunkRow;
71
+ /**
72
+ * Test whether an encoded record is a packed chunk row rather than a Session event.
73
+ * @param record - one persistence or bounded-history encoding record.
74
+ * @returns Whether the record is a packed chunk row.
75
+ */
76
+ export declare function isChunkRow(record: StorageRecord): record is ChunkRow;
77
+ /**
78
+ * Number of logical Session events represented by one packed row.
79
+ * @param row - validated or encoder-produced packed row.
80
+ * @returns Count of consecutive chunk events in the row.
81
+ */
82
+ export declare function chunkRowLength(row: ChunkRow): number;
70
83
  /**
71
84
  * Pack an event batch for storage: each run of at least {@link MIN_RUN}
72
85
  * consecutive whitelisted same-kind, same-block delta chunk events becomes one
@@ -1,23 +1,42 @@
1
1
  /**
2
- * Lossless storage packing for `assistant/chunk` delta runs. Providers stream
2
+ * Lossless row packing for `assistant/chunk` delta runs. Providers stream
3
3
  * token-sized deltas, so a log stores hundreds of near-identical event lines
4
4
  * whose JSON envelopes dwarf their payloads (~56× measured on a real DeepSeek
5
5
  * session). This module packs each run of consecutive same-block delta chunks
6
6
  * into ONE storage row — `text-chunks`, `reasoning-chunks`, or
7
7
  * `tool-call-chunks` — and expands rows back to the exact original events.
8
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.
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)
11
+ * type tags so a reader cannot confuse them with the event taxonomy
12
+ * (precedent: the JSONL header line's `session` tag). Persistence and bounded
13
+ * history transport both use the codec. The encoder whitelists exact shapes
14
+ * anything it does not fully recognize stays verbatim, so unknown fields or
15
+ * future chunk variants lose compression, never data. The decoder validates
16
+ * before expanding and fails loud on a malformed row-tagged value instead of
17
+ * silently dropping a whole run.
17
18
  *
18
19
  * @module @deepseek-ai/dsh-session/chunk-rows
19
20
  */
20
- import { CallId, assertNever } from '@deepseek-ai/dsh-llm';
21
+ import { brandString } from '@deepseek-ai/dsh-brand';
22
+ /**
23
+ * Test whether an encoded record is a packed chunk row rather than a Session event.
24
+ * @param record - one persistence or bounded-history encoding record.
25
+ * @returns Whether the record is a packed chunk row.
26
+ */
27
+ export function isChunkRow(record) {
28
+ return record.type === 'text-chunks'
29
+ || record.type === 'reasoning-chunks'
30
+ || record.type === 'tool-call-chunks';
31
+ }
32
+ /**
33
+ * Number of logical Session events represented by one packed row.
34
+ * @param row - validated or encoder-produced packed row.
35
+ * @returns Count of consecutive chunk events in the row.
36
+ */
37
+ export function chunkRowLength(row) {
38
+ return row.type === 'tool-call-chunks' ? row.data.args.length : row.data.texts.length;
39
+ }
21
40
  /**
22
41
  * Minimum members before a run packs. Below it a row's envelope rivals the
23
42
  * event lines it replaces. A format constant, not a tunable: both layouts
@@ -120,7 +139,7 @@ function buildRow(kind, run) {
120
139
  ...envelope,
121
140
  data: {
122
141
  ...base,
123
- id: CallId(call.id),
142
+ id: brandString(call.id),
124
143
  ...Object.hasOwn(call, 'name') ? { name: call.name } : {},
125
144
  args: run.map(event => event.data.chunk.argumentsDelta),
126
145
  },
@@ -231,7 +250,7 @@ function validateRow(value, tag) {
231
250
  // outside any encoder's image: float arithmetic would round it to a
232
251
  // different number than exact arithmetic, a silent corruption. Within safe
233
252
  // range every step is exact, so the first departure is always caught.
234
- if (!Number.isSafeInteger(value.seq0 + payload.length - 1)) {
253
+ if (payload.length - 1 > Number.MAX_SAFE_INTEGER - value.seq0) {
235
254
  malformed(tag, 'member seqs must stay safe integers');
236
255
  }
237
256
  let time = value.time0;
@@ -267,9 +286,11 @@ function expandRow(row) {
267
286
  argumentsDelta: members[k],
268
287
  };
269
288
  break;
270
- /* v8 ignore next 2 -- validateRow only returns the three row tags */
271
- default:
272
- return assertNever(row, 'chunk-rows expandRow');
289
+ /* v8 ignore next 4 -- validateRow only returns the three row tags */
290
+ default: {
291
+ const unreachable = row;
292
+ throw new Error(`chunk-rows received unsupported row ${String(unreachable)}`);
293
+ }
273
294
  }
274
295
  events.push({
275
296
  type: 'assistant/chunk',
@@ -8,16 +8,13 @@
8
8
  import { Context, Service } from '@deepseek-ai/cordis';
9
9
  import type { Scoped } from '@deepseek-ai/dsh-scope';
10
10
  import type { Message } from '@deepseek-ai/dsh-llm';
11
- import { SessionId } from './types.ts';
12
11
  import type { TypertLookup } from '@deepseek-ai/dsh-typert-protocol';
13
- import type { CreateSessionOptions, EpochHeader, PrepareSessionOptions, RequestContext, SessionEvent, SessionEventMap, SessionEventType, SessionHeader, SurfaceIntent, SurfaceEventType } from './types.ts';
12
+ import type { CreateSessionOptions, EpochHeader, PrepareSessionOptions, RequestContext, SessionEvent, SessionEventMap, SessionEventType, SessionHeader, SessionId, SurfaceIntent, SurfaceEventType } from './types.ts';
14
13
  import type { SessionSurface } from './surface.ts';
15
14
  export * from './types.ts';
16
15
  export { SessionPreparation } from './preparation.ts';
17
16
  export type { SessionPreparationOptions } from './preparation.ts';
18
17
  export type { AssistantMessage, ToolResultMessage, UserMessage } from '@deepseek-ai/dsh-llm';
19
- export { isJsonValue, snapshotJsonValue } from './json.ts';
20
- export type { JsonValue } from './json.ts';
21
18
  export { interruptedTurnClosers, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from './repair.ts';
22
19
  export { decodeStorageRecord, packChunkRuns } from './chunk-rows.ts';
23
20
  export type { ChunkRow, StorageRecord } from './chunk-rows.ts';
@@ -201,7 +198,7 @@ export declare class Session {
201
198
  * Map/Set/Date/class instance), or when the candidate violates the
202
199
  * canonical surface contract (marker shape and eligibility, unique
203
200
  * earlier source-event references, positional replacement validity, and complete
204
- * shadowed-node coverage). One recursive pass reads, validates, and
201
+ * shadowed-node coverage). One iterative pass reads, validates, and
205
202
  * copies each nested value once, so a stateful getter cannot supply one value
206
203
  * to validation and another to storage. The event log is the durable source
207
204
  * of truth, so a bad event fails at the append site rather than later during
@@ -414,5 +411,6 @@ export declare class SessionStore extends Service {
414
411
  private _forkSeed;
415
412
  private _resolveForkSource;
416
413
  }
414
+ export { decodeSeqRanges, encodeSeqRanges } from './seq-ranges.ts';
417
415
  export default SessionStore;
418
416
  //# sourceMappingURL=index.d.ts.map
@@ -7,15 +7,14 @@
7
7
  */
8
8
  import { Service } from '@deepseek-ai/cordis';
9
9
  import { isAbsolute } from 'node:path';
10
- import { deepFreeze } from '@deepseek-ai/dsh-llm';
10
+ import { brandString } from '@deepseek-ai/dsh-brand';
11
+ import { deepFreeze, snapshotJsonValue } from '@deepseek-ai/dsh-util-values';
11
12
  import { scopeOf, scopeTarget } from '@deepseek-ai/dsh-scope';
12
- import { SESSION_FORMAT_VERSION, SessionId } from "./types.js";
13
- import { snapshotJsonValue } from "./json.js";
13
+ import { SESSION_FORMAT_VERSION } from "./types.js";
14
14
  import { deriveEventMessage, SurfaceManager } from "./surface.js";
15
15
  import { foldRequestHeader } from "./request-header.js";
16
16
  export * from "./types.js";
17
17
  export { SessionPreparation } from "./preparation.js";
18
- export { isJsonValue, snapshotJsonValue } from "./json.js";
19
18
  export { interruptedTurnClosers, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from "./repair.js";
20
19
  export { decodeStorageRecord, packChunkRuns } from "./chunk-rows.js";
21
20
  export { deriveEventMessage, foldSurface, isAppendSurfaceEvent, isReplacementSurfaceEvent, isSurfaceEvent, isSurfaceEligibleType } from "./surface.js";
@@ -473,7 +472,7 @@ export class Session {
473
472
  * Map/Set/Date/class instance), or when the candidate violates the
474
473
  * canonical surface contract (marker shape and eligibility, unique
475
474
  * earlier source-event references, positional replacement validity, and complete
476
- * shadowed-node coverage). One recursive pass reads, validates, and
475
+ * shadowed-node coverage). One iterative pass reads, validates, and
477
476
  * copies each nested value once, so a stateful getter cannot supply one value
478
477
  * to validation and another to storage. The event log is the durable source
479
478
  * of truth, so a bad event fails at the append site rather than later during
@@ -716,11 +715,11 @@ export class SessionStore extends Service {
716
715
  let sessionId;
717
716
  if (id === undefined) {
718
717
  do
719
- sessionId = SessionId(`session-${++this.counter}`);
718
+ sessionId = brandString(`session-${++this.counter}`);
720
719
  while (this.store.has(sessionId));
721
720
  }
722
721
  else {
723
- sessionId = SessionId(id);
722
+ sessionId = brandString(id);
724
723
  }
725
724
  if (this.store.has(sessionId))
726
725
  throw new Error(`session "${sessionId}" already exists`);
@@ -995,5 +994,6 @@ export class SessionStore extends Service {
995
994
  return source;
996
995
  }
997
996
  }
997
+ export { decodeSeqRanges, encodeSeqRanges } from "./seq-ranges.js";
998
998
  export default SessionStore;
999
999
  //# sourceMappingURL=index.js.map
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * @module @deepseek-ai/dsh-session/invariant
6
6
  */
7
- import { assertNever } from '@deepseek-ai/dsh-llm';
7
+ import { assertNever } from '@deepseek-ai/dsh-util-values';
8
8
  import { TOOL_NOT_STARTED } from "./repair.js";
9
9
  const PACKAGE_NAME = '@deepseek-ai/dsh-session';
10
10
  /** Cordis companion plugin name. */
@@ -108,7 +108,6 @@ function validateEvent(trace, event, fail) {
108
108
  case 'session/end-seed':
109
109
  // Unconstrained: an unbalanced seed legally puts it inside an open turn.
110
110
  break;
111
- case 'todo/write':
112
111
  case 'request/header':
113
112
  case 'request/context': {
114
113
  if (trace.openTurn === null) {