@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/README.i18n.yaml +2 -2
- package/README.md +98 -57
- package/README.zh.md +104 -63
- package/lib/index.js +89 -193
- package/lib/invariant.js +2 -2
- package/lib/types/chunk-rows.d.ts +24 -11
- package/lib/types/chunk-rows.js +36 -15
- package/lib/types/index.d.ts +3 -5
- package/lib/types/index.js +7 -7
- package/lib/types/invariant.js +1 -2
- package/lib/types/known-event-types.d.ts +5 -2
- package/lib/types/known-event-types.js +8 -2
- package/lib/types/repair.js +4 -3
- package/lib/types/seq-ranges.d.ts +17 -0
- package/lib/types/seq-ranges.js +73 -0
- package/lib/types/types.d.ts +19 -28
- package/lib/types/types.js +3 -2
- package/package.json +17 -14
- package/lib/types/json.d.ts +0 -36
- package/lib/types/json.js +0 -174
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 {
|
|
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
|
|
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 =
|
|
670
|
-
id:
|
|
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
|
|
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
|
-
*
|
|
737
|
-
*
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
* verbatim, so unknown fields or
|
|
742
|
-
*
|
|
743
|
-
* row-tagged value instead of
|
|
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:
|
|
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 (
|
|
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
|
|
1005
|
-
default:
|
|
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
|
|
1052
|
-
*
|
|
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
|
|
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 =
|
|
1547
|
+
sessionId = brandString(`session-${++this.counter}`);
|
|
1652
1548
|
while (this.store.has(sessionId));
|
|
1653
|
-
else sessionId =
|
|
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,
|
|
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-
|
|
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
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* verbatim, so unknown fields or
|
|
15
|
-
*
|
|
16
|
-
* row-tagged value instead of
|
|
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 {
|
|
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:
|
|
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
|
package/lib/types/chunk-rows.js
CHANGED
|
@@ -1,23 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Lossless
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* verbatim, so unknown fields or
|
|
15
|
-
*
|
|
16
|
-
* row-tagged value instead of
|
|
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 {
|
|
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:
|
|
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 (
|
|
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
|
|
271
|
-
default:
|
|
272
|
-
|
|
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',
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/lib/types/index.js
CHANGED
|
@@ -7,15 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { Service } from '@deepseek-ai/cordis';
|
|
9
9
|
import { isAbsolute } from 'node:path';
|
|
10
|
-
import {
|
|
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
|
|
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
|
|
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 =
|
|
718
|
+
sessionId = brandString(`session-${++this.counter}`);
|
|
720
719
|
while (this.store.has(sessionId));
|
|
721
720
|
}
|
|
722
721
|
else {
|
|
723
|
-
sessionId =
|
|
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
|
package/lib/types/invariant.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @module @deepseek-ai/dsh-session/invariant
|
|
6
6
|
*/
|
|
7
|
-
import { assertNever } from '@deepseek-ai/dsh-
|
|
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) {
|