@rebon/cli-linux-x64 0.17.2 → 0.18.0
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/bin/compose-runtime/package.json +17 -0
- package/bin/compose-runtime/payload/compose/credentials-runtime.js +38 -0
- package/bin/compose-runtime/payload/compose/llm-runtime.js +330 -0
- package/bin/compose-runtime/payload/compose/loop-assembly.js +149 -0
- package/bin/compose-runtime/payload/compose/serve-dispatch.js +82 -0
- package/bin/compose-runtime/payload/compose/shims/dsh-anonymous-user-id.js +10 -0
- package/bin/compose-runtime/payload/compose/shims/dsh-credentials.js +13 -0
- package/bin/compose-runtime/payload/compose/shims/dsh-launch-environment.js +21 -0
- package/bin/compose-runtime/payload/compose/shims/dsh-llm.js +313 -0
- package/bin/compose-runtime/payload/compose/shims/dsh-settings.js +43 -0
- package/bin/compose-runtime/payload/compose/shims/dsh-tools.js +20 -0
- package/bin/compose-runtime/payload/compose/shims/dsh-web.js +10 -0
- package/bin/compose-runtime/payload/compose/shims/llm-adapter.js +32 -0
- package/bin/compose-runtime/payload/compose/shims/zod-lite.js +49 -0
- package/bin/compose-runtime/payload/compose/systemprompt-runtime.js +74 -0
- package/bin/compose-runtime/payload/compose/tools-runtime.js +228 -0
- package/bin/compose-runtime/payload/compose/web-runtime.js +204 -0
- package/bin/compose-runtime/payload/vendor/cordis/index.js +1530 -0
- package/bin/compose-runtime/payload/vendor/cosmokit/LICENSE +21 -0
- package/bin/compose-runtime/payload/vendor/cosmokit/index.mjs +357 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-agent +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-agent-loop +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-llm-core +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-llm-deepseek +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-logger-console +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-scope +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-session +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-system-prompt +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-timeout +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-timer +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-tool-todo +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-tool-web +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-tools-schema +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/LICENSE-web-search-exa +21 -0
- package/bin/compose-runtime/payload/vendor/dsh/agent-loop.js +1193 -0
- package/bin/compose-runtime/payload/vendor/dsh/agent.js +714 -0
- package/bin/compose-runtime/payload/vendor/dsh/llm-core.js +269 -0
- package/bin/compose-runtime/payload/vendor/dsh/llm-deepseek.js +672 -0
- package/bin/compose-runtime/payload/vendor/dsh/logger-console.js +83 -0
- package/bin/compose-runtime/payload/vendor/dsh/scope.js +287 -0
- package/bin/compose-runtime/payload/vendor/dsh/session.js +1668 -0
- package/bin/compose-runtime/payload/vendor/dsh/system-prompt.js +309 -0
- package/bin/compose-runtime/payload/vendor/dsh/timeout.js +100 -0
- package/bin/compose-runtime/payload/vendor/dsh/timer.js +128 -0
- package/bin/compose-runtime/payload/vendor/dsh/tool-todo.js +143 -0
- package/bin/compose-runtime/payload/vendor/dsh/tool-web.js +1527 -0
- package/bin/compose-runtime/payload/vendor/dsh/tools-schema.js +849 -0
- package/bin/compose-runtime/payload/vendor/dsh/web-search-exa.js +122 -0
- package/bin/compose-runtime/payload/vendor/eventsource-parser/LICENSE +21 -0
- package/bin/compose-runtime/payload/vendor/eventsource-parser/index.js +177 -0
- package/bin/compose-runtime/payload/vendor/eventsource-parser/stream.js +48 -0
- package/bin/compose-runtime/payload/vendor/schemastery/index.mjs +656 -0
- package/bin/compose-runtime/payload-manifests.json +48 -0
- package/bin/compose-runtime/src/bridge.mjs +142 -0
- package/bin/compose-runtime/src/credentials-runtime.mjs +50 -0
- package/bin/compose-runtime/src/eventsource-stream.mjs +48 -0
- package/bin/compose-runtime/src/index.mjs +7 -0
- package/bin/compose-runtime/src/llm-runtime.mjs +326 -0
- package/bin/compose-runtime/src/loader.mjs +78 -0
- package/bin/compose-runtime/src/loop-assembly.mjs +237 -0
- package/bin/compose-runtime/src/payload.mjs +49 -0
- package/bin/compose-runtime/src/plugin.mjs +59 -0
- package/bin/compose-runtime/src/realm.mjs +223 -0
- package/bin/compose-runtime/src/registry.mjs +191 -0
- package/bin/compose-runtime/src/resolve.mjs +115 -0
- package/bin/compose-runtime/src/systemprompt-runtime.mjs +67 -0
- package/bin/compose-runtime/src/tools-runtime.mjs +239 -0
- package/bin/compose-runtime/src/web-runtime.mjs +209 -0
- package/bin/plugin-host/package.json +11 -0
- package/bin/plugin-host/src/bridge.mjs +64 -0
- package/bin/plugin-host/src/cli.mjs +105 -0
- package/bin/plugin-host/src/framing.mjs +113 -0
- package/bin/plugin-host/src/host.mjs +624 -0
- package/bin/plugin-host/src/json.mjs +196 -0
- package/bin/plugin-host/src/lifecycle.mjs +114 -0
- package/bin/plugin-host/src/loader.mjs +142 -0
- package/bin/plugin-host/src/methods.mjs +256 -0
- package/bin/plugin-host/src/protocol.mjs +129 -0
- package/bin/plugin-host/src/sdk.mjs +7 -0
- package/bin/rebon +0 -0
- package/bin/rebon-boa-helper +0 -0
- package/package.json +4 -1
|
@@ -0,0 +1,1668 @@
|
|
|
1
|
+
// packages/core/session/src/index.ts
|
|
2
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
3
|
+
import { isAbsolute } from "node:path";
|
|
4
|
+
import { deepFreeze } from "@deepseek-ai/dsh-llm";
|
|
5
|
+
import { scopeOf, scopeTarget } from "@deepseek-ai/dsh-scope";
|
|
6
|
+
|
|
7
|
+
// packages/core/session/src/types.ts
|
|
8
|
+
function SessionId(id) {
|
|
9
|
+
return id;
|
|
10
|
+
}
|
|
11
|
+
var SESSION_FORMAT_VERSION = 0;
|
|
12
|
+
|
|
13
|
+
// packages/core/session/src/json.ts
|
|
14
|
+
function hasIntrinsicConstructor(prototype, name) {
|
|
15
|
+
const descriptor = Object.getOwnPropertyDescriptor(prototype, "constructor");
|
|
16
|
+
const constructor = descriptor?.value;
|
|
17
|
+
if (typeof constructor !== "function") return false;
|
|
18
|
+
try {
|
|
19
|
+
return constructor.name === name && constructor.prototype === prototype && Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function isIntrinsicObjectPrototype(value) {
|
|
25
|
+
return Object.getPrototypeOf(value) === null && hasIntrinsicConstructor(value, "Object");
|
|
26
|
+
}
|
|
27
|
+
function hasPlainArrayPrototype(value) {
|
|
28
|
+
const prototype = Object.getPrototypeOf(value);
|
|
29
|
+
if (!Array.isArray(prototype) || !hasIntrinsicConstructor(prototype, "Array")) return false;
|
|
30
|
+
const objectPrototype = Object.getPrototypeOf(prototype);
|
|
31
|
+
return typeof objectPrototype === "object" && objectPrototype !== null && isIntrinsicObjectPrototype(objectPrototype);
|
|
32
|
+
}
|
|
33
|
+
function hasPlainObjectPrototype(value) {
|
|
34
|
+
const prototype = Object.getPrototypeOf(value);
|
|
35
|
+
return prototype === null || typeof prototype === "object" && isIntrinsicObjectPrototype(prototype);
|
|
36
|
+
}
|
|
37
|
+
function enumerableStringKeys(value) {
|
|
38
|
+
const keys = Reflect.ownKeys(value);
|
|
39
|
+
if (keys.some((key) => typeof key !== "string" || !Object.prototype.propertyIsEnumerable.call(value, key))) return void 0;
|
|
40
|
+
return keys;
|
|
41
|
+
}
|
|
42
|
+
function walkJsonValue(value, detach) {
|
|
43
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
44
|
+
let root;
|
|
45
|
+
const assign = (destination, item) => {
|
|
46
|
+
if (destination === void 0) return;
|
|
47
|
+
if (destination.kind === "root") {
|
|
48
|
+
root = item;
|
|
49
|
+
} else if (destination.kind === "array") {
|
|
50
|
+
destination.target[destination.index] = item;
|
|
51
|
+
} else {
|
|
52
|
+
Object.defineProperty(destination.target, destination.key, {
|
|
53
|
+
value: item,
|
|
54
|
+
enumerable: true,
|
|
55
|
+
configurable: true,
|
|
56
|
+
writable: true
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const tasks = [{
|
|
61
|
+
kind: "visit",
|
|
62
|
+
value,
|
|
63
|
+
...detach ? { destination: { kind: "root" } } : {}
|
|
64
|
+
}];
|
|
65
|
+
for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
|
|
66
|
+
if (task.kind === "leave") {
|
|
67
|
+
ancestors.delete(task.source);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (task.kind === "array-item") {
|
|
71
|
+
if (!Object.prototype.hasOwnProperty.call(task.source, task.index)) return void 0;
|
|
72
|
+
tasks.push({
|
|
73
|
+
kind: "visit",
|
|
74
|
+
value: task.source[task.index],
|
|
75
|
+
...task.target === void 0 ? {} : { destination: { kind: "array", target: task.target, index: task.index } }
|
|
76
|
+
});
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (task.kind === "object-property") {
|
|
80
|
+
tasks.push({
|
|
81
|
+
kind: "visit",
|
|
82
|
+
value: task.source[task.key],
|
|
83
|
+
...task.target === void 0 ? {} : { destination: { kind: "object", target: task.target, key: task.key } }
|
|
84
|
+
});
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const current = task.value;
|
|
88
|
+
if (current === null) {
|
|
89
|
+
assign(task.destination, null);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (typeof current === "boolean" || typeof current === "string") {
|
|
93
|
+
assign(task.destination, current);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (typeof current === "number") {
|
|
97
|
+
if (!Number.isFinite(current) || Object.is(current, -0)) return void 0;
|
|
98
|
+
assign(task.destination, current);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (typeof current !== "object") return void 0;
|
|
102
|
+
if (ancestors.has(current)) return void 0;
|
|
103
|
+
if (Array.isArray(current)) {
|
|
104
|
+
if (!hasPlainArrayPrototype(current)) return void 0;
|
|
105
|
+
const length = current.length;
|
|
106
|
+
if (Reflect.ownKeys(current).length !== length + 1) return void 0;
|
|
107
|
+
const target2 = detach ? [] : void 0;
|
|
108
|
+
if (target2 !== void 0) assign(task.destination, target2);
|
|
109
|
+
ancestors.add(current);
|
|
110
|
+
tasks.push({ kind: "leave", source: current });
|
|
111
|
+
for (let index = length - 1; index >= 0; index--) {
|
|
112
|
+
tasks.push({ kind: "array-item", source: current, index, ...target2 === void 0 ? {} : { target: target2 } });
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (!hasPlainObjectPrototype(current)) return void 0;
|
|
117
|
+
const keys = enumerableStringKeys(current);
|
|
118
|
+
if (keys === void 0) return void 0;
|
|
119
|
+
const target = detach ? {} : void 0;
|
|
120
|
+
if (target !== void 0) assign(task.destination, target);
|
|
121
|
+
ancestors.add(current);
|
|
122
|
+
tasks.push({ kind: "leave", source: current });
|
|
123
|
+
for (let index = keys.length - 1; index >= 0; index--) {
|
|
124
|
+
const key = keys[index];
|
|
125
|
+
if (key === void 0) return void 0;
|
|
126
|
+
tasks.push({ kind: "object-property", source: current, key, ...target === void 0 ? {} : { target } });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return detach ? root : true;
|
|
130
|
+
}
|
|
131
|
+
function snapshotJsonValue(value) {
|
|
132
|
+
return walkJsonValue(value, true);
|
|
133
|
+
}
|
|
134
|
+
function isJsonValue(value) {
|
|
135
|
+
return walkJsonValue(value, false) === true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// packages/core/session/src/surface.ts
|
|
139
|
+
var SURFACE_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
140
|
+
"user/message",
|
|
141
|
+
"assistant/message",
|
|
142
|
+
"tool/result"
|
|
143
|
+
]);
|
|
144
|
+
function isSurfaceEligibleType(type) {
|
|
145
|
+
return SURFACE_EVENT_TYPES.has(type);
|
|
146
|
+
}
|
|
147
|
+
function isSurfaceEvent(event) {
|
|
148
|
+
if (!SURFACE_EVENT_TYPES.has(event.type)) return false;
|
|
149
|
+
return event.surfaceOp !== void 0;
|
|
150
|
+
}
|
|
151
|
+
function isAppendSurfaceEvent(event) {
|
|
152
|
+
return isSurfaceEvent(event) && event.surfaceOp === "append";
|
|
153
|
+
}
|
|
154
|
+
function isReplacementSurfaceEvent(event) {
|
|
155
|
+
return isSurfaceEvent(event) && event.surfaceOp !== "append";
|
|
156
|
+
}
|
|
157
|
+
function deriveEventMessage(event) {
|
|
158
|
+
switch (event.type) {
|
|
159
|
+
// Ordinary prompts and injected context project in user role: the event's
|
|
160
|
+
// model-facing content stays verbatim. Do NOT re-add per-type framing
|
|
161
|
+
// (e.g. `<context>`) here: framing is caller-owned — a producer bakes it
|
|
162
|
+
// into `content`, as agent-instructions does with `<system-reminder>` — or,
|
|
163
|
+
// if reintroduced, must be driven by the event `meta` map and a dedicated
|
|
164
|
+
// renderer, keeping this projection a verbatim pass-through. See the
|
|
165
|
+
// deferred design note in
|
|
166
|
+
// ../../../../.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.md
|
|
167
|
+
case "user/message": {
|
|
168
|
+
return event.data;
|
|
169
|
+
}
|
|
170
|
+
case "assistant/message": {
|
|
171
|
+
if (event.data.message.content.length === 0) return null;
|
|
172
|
+
return event.data.message;
|
|
173
|
+
}
|
|
174
|
+
case "tool/result": {
|
|
175
|
+
return event.data.message;
|
|
176
|
+
}
|
|
177
|
+
default:
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function createFoldState() {
|
|
182
|
+
return { nodes: [], replaceGeneration: 0 };
|
|
183
|
+
}
|
|
184
|
+
function isEventSeq(value) {
|
|
185
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
186
|
+
}
|
|
187
|
+
function isReplaceOp(value) {
|
|
188
|
+
const op = value;
|
|
189
|
+
return Object.keys(op).length === 3 && Object.hasOwn(op, "op") && Object.hasOwn(op, "start") && Object.hasOwn(op, "end") && op["op"] === "replace" && isEventSeq(op["start"]) && isEventSeq(op["end"]);
|
|
190
|
+
}
|
|
191
|
+
function surfaceOpOf(event) {
|
|
192
|
+
const raw = event;
|
|
193
|
+
if (!isSurfaceEligibleType(event.type)) {
|
|
194
|
+
if (raw.surfaceOp !== void 0) {
|
|
195
|
+
throw new Error(`session event "${event.type}" is not surface-eligible and cannot carry surfaceOp`);
|
|
196
|
+
}
|
|
197
|
+
if (raw.sourceEventSeqs !== void 0) {
|
|
198
|
+
throw new Error(`session event "${event.type}" is not surface-eligible and cannot carry sourceEventSeqs`);
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const op = raw.surfaceOp;
|
|
203
|
+
if (op === void 0) {
|
|
204
|
+
throw new Error(`session event "${event.type}" is surface-eligible and requires a surfaceOp marker`);
|
|
205
|
+
}
|
|
206
|
+
if (op === "append") return op;
|
|
207
|
+
if (op === null || typeof op !== "object" || Array.isArray(op)) {
|
|
208
|
+
throw new Error(`session event "${event.type}" carries an invalid surfaceOp`);
|
|
209
|
+
}
|
|
210
|
+
if (!isReplaceOp(op)) {
|
|
211
|
+
throw new Error(`session event "${event.type}" carries an invalid replace surfaceOp`);
|
|
212
|
+
}
|
|
213
|
+
return op;
|
|
214
|
+
}
|
|
215
|
+
function assertProvenance(event, shadowedSeqs) {
|
|
216
|
+
const raw = event.sourceEventSeqs;
|
|
217
|
+
const sources = /* @__PURE__ */ new Set();
|
|
218
|
+
if (raw !== void 0) {
|
|
219
|
+
if (!Array.isArray(raw)) {
|
|
220
|
+
throw new Error(`sourceEventSeqs on event at seq ${event.seq} must be an array when present`);
|
|
221
|
+
}
|
|
222
|
+
if (raw.length === 0 && event.type !== "assistant/message") {
|
|
223
|
+
throw new Error("sourceEventSeqs must not be empty except on assistant/message");
|
|
224
|
+
}
|
|
225
|
+
let nonEarlierSource;
|
|
226
|
+
for (const source of raw) {
|
|
227
|
+
if (!isEventSeq(source)) {
|
|
228
|
+
throw new Error(`session event "${event.type}" sourceEventSeqs must densely contain non-negative safe integers`);
|
|
229
|
+
}
|
|
230
|
+
sources.add(source);
|
|
231
|
+
if (nonEarlierSource === void 0 && source >= event.seq) nonEarlierSource = source;
|
|
232
|
+
}
|
|
233
|
+
if (sources.size !== raw.length) {
|
|
234
|
+
throw new Error("sourceEventSeqs must not contain duplicates");
|
|
235
|
+
}
|
|
236
|
+
if (nonEarlierSource !== void 0) {
|
|
237
|
+
throw new Error(`sourceEventSeqs must reference earlier events: ${nonEarlierSource} >= current seq ${event.seq}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const missing = shadowedSeqs.filter((seq) => !sources.has(seq));
|
|
241
|
+
if (missing.length > 0) {
|
|
242
|
+
throw new Error(`surface replace: sourceEventSeqs must include every shadowed surface node; missing ${missing.join(", ")}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function replacementRange(state, op) {
|
|
246
|
+
const startIdx = state.nodes.indexOf(op.start);
|
|
247
|
+
if (startIdx === -1) {
|
|
248
|
+
throw new Error(`surface replace: start seq ${op.start} not found in surface`);
|
|
249
|
+
}
|
|
250
|
+
const endIdx = state.nodes.indexOf(op.end);
|
|
251
|
+
if (endIdx === -1) {
|
|
252
|
+
throw new Error(`surface replace: end seq ${op.end} not found in surface`);
|
|
253
|
+
}
|
|
254
|
+
if (startIdx > endIdx) {
|
|
255
|
+
throw new Error(`surface replace: start seq ${op.start} (index ${startIdx}) is after end seq ${op.end} (index ${endIdx})`);
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
startIdx,
|
|
259
|
+
endIdx,
|
|
260
|
+
shadowedSeqs: state.nodes.slice(startIdx, endIdx + 1)
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function isDeepEqualJson(a, b) {
|
|
264
|
+
if (a === b) return true;
|
|
265
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
266
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
267
|
+
return a.every((item, i) => isDeepEqualJson(item, b[i]));
|
|
268
|
+
}
|
|
269
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
270
|
+
const aKeys = Object.keys(a);
|
|
271
|
+
const bRecord = b;
|
|
272
|
+
if (aKeys.length !== Object.keys(b).length) return false;
|
|
273
|
+
return aKeys.every((key) => Object.hasOwn(b, key) && isDeepEqualJson(a[key], bRecord[key]));
|
|
274
|
+
}
|
|
275
|
+
function assertToolResultRewrite(event, shadowedSeqs, events, baseSeq) {
|
|
276
|
+
if (event.type !== "tool/result") return;
|
|
277
|
+
if (shadowedSeqs.length !== 1) {
|
|
278
|
+
throw new Error("tool/result surface replacement must rewrite exactly one current node");
|
|
279
|
+
}
|
|
280
|
+
for (const originalSeq of shadowedSeqs) {
|
|
281
|
+
const original = events[originalSeq - baseSeq];
|
|
282
|
+
if (original?.type !== "tool/result") {
|
|
283
|
+
throw new Error("tool/result surface replacement must target a current tool/result");
|
|
284
|
+
}
|
|
285
|
+
const originalRest = { ...original.data };
|
|
286
|
+
const replacementRest = { ...event.data };
|
|
287
|
+
const originalResult = original.data.message.content[0];
|
|
288
|
+
const replacementResult = event.data.message.content[0];
|
|
289
|
+
originalRest["message"] = {
|
|
290
|
+
...original.data.message,
|
|
291
|
+
content: [{ ...originalResult, content: null }]
|
|
292
|
+
};
|
|
293
|
+
replacementRest["message"] = {
|
|
294
|
+
...event.data.message,
|
|
295
|
+
content: [{ ...replacementResult, content: null }]
|
|
296
|
+
};
|
|
297
|
+
if (!isDeepEqualJson(originalRest, replacementRest)) {
|
|
298
|
+
throw new Error("tool/result surface replacement may change only content");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function planSurfaceEvent(state, event, expectedSeq, events, baseSeq) {
|
|
303
|
+
if (event.seq !== expectedSeq) {
|
|
304
|
+
throw new Error(`session event seq ${event.seq} is not contiguous; expected ${expectedSeq}`);
|
|
305
|
+
}
|
|
306
|
+
const surfaceOp = surfaceOpOf(event);
|
|
307
|
+
if (surfaceOp === void 0) return;
|
|
308
|
+
if (surfaceOp === "append") {
|
|
309
|
+
assertProvenance(event, []);
|
|
310
|
+
return { kind: "append", seq: event.seq };
|
|
311
|
+
}
|
|
312
|
+
const range = replacementRange(state, surfaceOp);
|
|
313
|
+
assertProvenance(event, range.shadowedSeqs);
|
|
314
|
+
assertToolResultRewrite(event, range.shadowedSeqs, events, baseSeq);
|
|
315
|
+
return {
|
|
316
|
+
kind: "replace",
|
|
317
|
+
seq: event.seq,
|
|
318
|
+
start: surfaceOp.start,
|
|
319
|
+
end: surfaceOp.end,
|
|
320
|
+
...range
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
function applySurfaceEvent(state, event, expectedSeq, events, baseSeq) {
|
|
324
|
+
const plan = planSurfaceEvent(state, event, expectedSeq, events, baseSeq);
|
|
325
|
+
return applySurfacePlan(state, plan);
|
|
326
|
+
}
|
|
327
|
+
function applySurfacePlan(state, plan) {
|
|
328
|
+
if (plan?.kind === "append") {
|
|
329
|
+
state.nodes.push(plan.seq);
|
|
330
|
+
} else if (plan?.kind === "replace") {
|
|
331
|
+
state.nodes.splice(plan.startIdx, plan.endIdx - plan.startIdx + 1, plan.seq);
|
|
332
|
+
state.replaceGeneration += 1;
|
|
333
|
+
}
|
|
334
|
+
if (plan?.kind !== "replace") return;
|
|
335
|
+
return {
|
|
336
|
+
seq: plan.seq,
|
|
337
|
+
start: plan.start,
|
|
338
|
+
end: plan.end,
|
|
339
|
+
shadowedSeqs: plan.shadowedSeqs
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function foldSurface(events) {
|
|
343
|
+
const state = createFoldState();
|
|
344
|
+
const replacements = [];
|
|
345
|
+
for (const [index, event] of events.entries()) {
|
|
346
|
+
const replacement = applySurfaceEvent(state, event, index, events, 0);
|
|
347
|
+
if (replacement !== void 0) replacements.push(replacement);
|
|
348
|
+
}
|
|
349
|
+
return { nodes: [...state.nodes], replacements };
|
|
350
|
+
}
|
|
351
|
+
var SurfaceManager = class {
|
|
352
|
+
/**
|
|
353
|
+
* @param log - Contiguous complete log or loaded event window.
|
|
354
|
+
* @param baseSeq - Absolute sequence of the window's first event.
|
|
355
|
+
*/
|
|
356
|
+
constructor(log, baseSeq = 0) {
|
|
357
|
+
this.log = log;
|
|
358
|
+
this.baseSeq = baseSeq;
|
|
359
|
+
this._lastProcessedSeq = baseSeq - 1;
|
|
360
|
+
}
|
|
361
|
+
log;
|
|
362
|
+
baseSeq;
|
|
363
|
+
/** Shared transition state; replacement history is not retained. */
|
|
364
|
+
_state = createFoldState();
|
|
365
|
+
/** Last processed absolute seq. */
|
|
366
|
+
_lastProcessedSeq;
|
|
367
|
+
/** Candidate already validated by `validateNext`, pending exact log admission. */
|
|
368
|
+
_pendingPlan;
|
|
369
|
+
/**
|
|
370
|
+
* Validate the next candidate without mutating the committed surface.
|
|
371
|
+
* @param event - candidate event that has not entered the log yet.
|
|
372
|
+
*/
|
|
373
|
+
validateNext(event) {
|
|
374
|
+
if (this._lastProcessedSeq < this.baseSeq + this.log.length - 1) this._processDelta();
|
|
375
|
+
const expectedSeq = this.baseSeq + this.log.length;
|
|
376
|
+
this._pendingPlan = {
|
|
377
|
+
event,
|
|
378
|
+
expectedSeq,
|
|
379
|
+
plan: planSurfaceEvent(this._state, event, expectedSeq, this.log, this.baseSeq)
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
/** Monotonic count of folded positional replacements. */
|
|
383
|
+
get replaceGeneration() {
|
|
384
|
+
if (this._lastProcessedSeq < this.baseSeq + this.log.length - 1) this._processDelta();
|
|
385
|
+
return this._state.replaceGeneration;
|
|
386
|
+
}
|
|
387
|
+
/** Surface event sequences in model-visible order. */
|
|
388
|
+
get nodes() {
|
|
389
|
+
if (this._lastProcessedSeq < this.baseSeq + this.log.length - 1) this._processDelta();
|
|
390
|
+
return this._state.nodes;
|
|
391
|
+
}
|
|
392
|
+
/** Fold events appended since the previous access. */
|
|
393
|
+
_processDelta() {
|
|
394
|
+
const tailSeq = this.baseSeq + this.log.length - 1;
|
|
395
|
+
for (let seq = this._lastProcessedSeq + 1; seq <= tailSeq; seq++) {
|
|
396
|
+
const index = seq - this.baseSeq;
|
|
397
|
+
const event = this.log[index];
|
|
398
|
+
const pending = this._pendingPlan;
|
|
399
|
+
if (pending?.event === event && pending.expectedSeq === seq) {
|
|
400
|
+
applySurfacePlan(this._state, pending.plan);
|
|
401
|
+
} else {
|
|
402
|
+
applySurfaceEvent(this._state, event, seq, this.log, this.baseSeq);
|
|
403
|
+
}
|
|
404
|
+
if (pending !== void 0 && pending.expectedSeq <= seq) this._pendingPlan = void 0;
|
|
405
|
+
this._lastProcessedSeq = seq;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// packages/core/session/src/request-header.ts
|
|
411
|
+
import { callConfigEquals } from "@deepseek-ai/dsh-llm";
|
|
412
|
+
function canonicalHeader(header) {
|
|
413
|
+
const adapterDefaults = header.adapterDefaults;
|
|
414
|
+
return {
|
|
415
|
+
config: header.config,
|
|
416
|
+
...adapterDefaults?.reasoningEffort === true || adapterDefaults?.maxTokens === true ? { adapterDefaults } : {},
|
|
417
|
+
...header.system !== void 0 && header.system.length > 0 ? { system: header.system } : {},
|
|
418
|
+
...header.tools !== void 0 && header.tools.length > 0 ? { tools: header.tools } : {}
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function sameSchema(a, b) {
|
|
422
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
423
|
+
}
|
|
424
|
+
function headerEquals(a, b) {
|
|
425
|
+
if (!callConfigEquals(a.config, b.config) || a.adapterDefaults?.reasoningEffort !== b.adapterDefaults?.reasoningEffort || a.adapterDefaults?.maxTokens !== b.adapterDefaults?.maxTokens || a.system !== b.system) return false;
|
|
426
|
+
const at = a.tools ?? [];
|
|
427
|
+
const bt = b.tools ?? [];
|
|
428
|
+
return at.length === bt.length && at.every((tool, i) => sameSchema(tool, bt[i]));
|
|
429
|
+
}
|
|
430
|
+
function foldRequestHeader(events, from) {
|
|
431
|
+
let state = from;
|
|
432
|
+
for (const event of events) {
|
|
433
|
+
if (event.type === "request/header") state = canonicalHeader(event.data.header);
|
|
434
|
+
}
|
|
435
|
+
return state;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// packages/core/session/src/preparation.ts
|
|
439
|
+
var SessionPreparation = class _SessionPreparation {
|
|
440
|
+
constructor(session, options) {
|
|
441
|
+
this.options = options;
|
|
442
|
+
this.session = session;
|
|
443
|
+
}
|
|
444
|
+
options;
|
|
445
|
+
released = false;
|
|
446
|
+
/** The exact Session to use for setup and publication. */
|
|
447
|
+
session;
|
|
448
|
+
/**
|
|
449
|
+
* Wrap an unpublished Session in one preparation lifetime.
|
|
450
|
+
* @param session - exact unpublished Session.
|
|
451
|
+
* @param options - optional provider release behavior.
|
|
452
|
+
* @returns a preparation disposed after publication or rollback.
|
|
453
|
+
*/
|
|
454
|
+
static create(session, options) {
|
|
455
|
+
return new _SessionPreparation(session, options ?? {});
|
|
456
|
+
}
|
|
457
|
+
/** Release provider state once when this preparation leaves its caller. */
|
|
458
|
+
[Symbol.dispose]() {
|
|
459
|
+
if (this.released) return;
|
|
460
|
+
this.released = true;
|
|
461
|
+
this.options.release?.();
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
// packages/core/session/src/repair.ts
|
|
466
|
+
import { MessageId, freezeMessage } from "@deepseek-ai/dsh-llm";
|
|
467
|
+
var TOOL_NOT_STARTED = "TOOL_NOT_STARTED";
|
|
468
|
+
var TOOL_OUTCOME_UNKNOWN = "TOOL_OUTCOME_UNKNOWN";
|
|
469
|
+
function interruptedTurnClosers(events) {
|
|
470
|
+
let openTurn = null;
|
|
471
|
+
let openStep = null;
|
|
472
|
+
const pendingCalls = /* @__PURE__ */ new Map();
|
|
473
|
+
for (const event of events) {
|
|
474
|
+
switch (event.type) {
|
|
475
|
+
case "turn/start":
|
|
476
|
+
openTurn = event.data.turn;
|
|
477
|
+
openStep = null;
|
|
478
|
+
pendingCalls.clear();
|
|
479
|
+
break;
|
|
480
|
+
case "turn/end":
|
|
481
|
+
openTurn = null;
|
|
482
|
+
openStep = null;
|
|
483
|
+
pendingCalls.clear();
|
|
484
|
+
break;
|
|
485
|
+
case "step/start":
|
|
486
|
+
openStep = event.data.step;
|
|
487
|
+
break;
|
|
488
|
+
case "step/end":
|
|
489
|
+
pendingCalls.clear();
|
|
490
|
+
openStep = null;
|
|
491
|
+
break;
|
|
492
|
+
case "assistant/message":
|
|
493
|
+
for (const block of event.data.message.content) {
|
|
494
|
+
if (block.type === "tool-call") pendingCalls.set(block.id, { step: event.data.step });
|
|
495
|
+
}
|
|
496
|
+
break;
|
|
497
|
+
case "tool/call":
|
|
498
|
+
{
|
|
499
|
+
const entry = pendingCalls.get(event.data.callId);
|
|
500
|
+
if (entry) {
|
|
501
|
+
entry.callSeq = event.seq;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
break;
|
|
505
|
+
case "tool/result":
|
|
506
|
+
pendingCalls.delete(event.data.message.source.callId);
|
|
507
|
+
break;
|
|
508
|
+
// Other event types do not move the turn/step boundary cursor.
|
|
509
|
+
default:
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const last = events.at(-1);
|
|
514
|
+
if (openTurn === null || last === void 0) return [];
|
|
515
|
+
let seq = last.seq + 1;
|
|
516
|
+
const time = last.time;
|
|
517
|
+
const closers = [];
|
|
518
|
+
for (const [callId, { step, callSeq }] of pendingCalls) {
|
|
519
|
+
const started = callSeq !== void 0;
|
|
520
|
+
const message = freezeMessage({
|
|
521
|
+
id: MessageId(`interrupted-tool-result-${callId}-${seq}`),
|
|
522
|
+
role: "user",
|
|
523
|
+
source: { kind: "tool", callId },
|
|
524
|
+
content: [{
|
|
525
|
+
type: "tool-result",
|
|
526
|
+
toolCallId: callId,
|
|
527
|
+
isError: true,
|
|
528
|
+
content: [{
|
|
529
|
+
type: "text",
|
|
530
|
+
text: started ? "The tool call was interrupted after it was recorded, but no result was durably recorded. Its outcome is unknown. Decide whether to retry from the tool semantics: retry only if the operation is read-only or idempotent; if it may have side effects, first verify external state or ask the user. Do not retry blindly." : "The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed."
|
|
531
|
+
}]
|
|
532
|
+
}]
|
|
533
|
+
});
|
|
534
|
+
closers.push({
|
|
535
|
+
type: "tool/result",
|
|
536
|
+
seq: seq++,
|
|
537
|
+
time,
|
|
538
|
+
data: {
|
|
539
|
+
turn: openTurn,
|
|
540
|
+
step,
|
|
541
|
+
message,
|
|
542
|
+
error: started ? { name: "ToolOutcomeUnknownError", code: TOOL_OUTCOME_UNKNOWN } : { name: "ToolNotStartedError", code: TOOL_NOT_STARTED }
|
|
543
|
+
},
|
|
544
|
+
surfaceOp: "append",
|
|
545
|
+
...started ? { sourceEventSeqs: [callSeq] } : {}
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
if (openStep !== null) {
|
|
549
|
+
closers.push({ type: "step/end", seq: seq++, time, data: { turn: openTurn, step: openStep } });
|
|
550
|
+
}
|
|
551
|
+
closers.push({ type: "turn/end", seq: seq++, time, data: { turn: openTurn, reason: { kind: "interrupted" } } });
|
|
552
|
+
return closers;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// packages/core/session/src/chunk-rows.ts
|
|
556
|
+
import { CallId, assertNever } from "@deepseek-ai/dsh-llm";
|
|
557
|
+
var MIN_RUN = 3;
|
|
558
|
+
function isRecord(value) {
|
|
559
|
+
return typeof value === "object" && value !== null;
|
|
560
|
+
}
|
|
561
|
+
function hasExactKeys(value, keys) {
|
|
562
|
+
return Object.keys(value).length === keys.length && keys.every((k) => Object.hasOwn(value, k));
|
|
563
|
+
}
|
|
564
|
+
function classify(event) {
|
|
565
|
+
if (event.type !== "assistant/chunk") return void 0;
|
|
566
|
+
if (!hasExactKeys(event, ["type", "seq", "time", "data"])) return void 0;
|
|
567
|
+
if (!Number.isSafeInteger(event.seq) || event.seq < 0 || !Number.isSafeInteger(event.time)) return void 0;
|
|
568
|
+
const data = event.data;
|
|
569
|
+
if (!isRecord(data) || !hasExactKeys(data, ["turn", "step", "chunk"])) return void 0;
|
|
570
|
+
if (typeof data.turn !== "number" || typeof data.step !== "number") return void 0;
|
|
571
|
+
const chunk = data.chunk;
|
|
572
|
+
if (!isRecord(chunk) || typeof chunk.index !== "number") return void 0;
|
|
573
|
+
switch (chunk.type) {
|
|
574
|
+
case "text-delta":
|
|
575
|
+
case "reasoning-delta":
|
|
576
|
+
return hasExactKeys(chunk, ["type", "index", "text"]) && typeof chunk.text === "string" ? chunk.type : void 0;
|
|
577
|
+
case "tool-call-delta": {
|
|
578
|
+
const shapeOk = hasExactKeys(chunk, ["type", "index", "id", "argumentsDelta"]) || hasExactKeys(chunk, ["type", "index", "id", "name", "argumentsDelta"]) && typeof chunk.name === "string";
|
|
579
|
+
return shapeOk && typeof chunk.id === "string" && typeof chunk.argumentsDelta === "string" ? chunk.type : void 0;
|
|
580
|
+
}
|
|
581
|
+
// Whitelist fall-through over parsed data: block-start/end, usage, finish,
|
|
582
|
+
// and any future chunk variant stay one event per line.
|
|
583
|
+
default:
|
|
584
|
+
return void 0;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
function toolCallOf(event) {
|
|
588
|
+
return event.data.chunk;
|
|
589
|
+
}
|
|
590
|
+
function indexOf(event) {
|
|
591
|
+
return event.data.chunk.index;
|
|
592
|
+
}
|
|
593
|
+
function continues(prev, next, kind) {
|
|
594
|
+
if (next.seq !== prev.seq + 1) return false;
|
|
595
|
+
if (!Number.isSafeInteger(next.time - prev.time)) return false;
|
|
596
|
+
if (next.data.turn !== prev.data.turn || next.data.step !== prev.data.step) return false;
|
|
597
|
+
if (indexOf(next) !== indexOf(prev)) return false;
|
|
598
|
+
if (kind !== "tool-call-delta") return true;
|
|
599
|
+
const a = toolCallOf(prev);
|
|
600
|
+
const b = toolCallOf(next);
|
|
601
|
+
return a.id === b.id && Object.hasOwn(a, "name") === Object.hasOwn(b, "name") && a.name === b.name;
|
|
602
|
+
}
|
|
603
|
+
function buildRow(kind, run) {
|
|
604
|
+
const first = run[0];
|
|
605
|
+
const base = {
|
|
606
|
+
turn: first.data.turn,
|
|
607
|
+
step: first.data.step,
|
|
608
|
+
index: indexOf(first),
|
|
609
|
+
dt: run.slice(1).map((event, i) => event.time - run[i].time)
|
|
610
|
+
};
|
|
611
|
+
const envelope = { seq0: first.seq, time0: first.time };
|
|
612
|
+
if (kind === "tool-call-delta") {
|
|
613
|
+
const call = toolCallOf(first);
|
|
614
|
+
return {
|
|
615
|
+
type: "tool-call-chunks",
|
|
616
|
+
...envelope,
|
|
617
|
+
data: {
|
|
618
|
+
...base,
|
|
619
|
+
id: CallId(call.id),
|
|
620
|
+
...Object.hasOwn(call, "name") ? { name: call.name } : {},
|
|
621
|
+
args: run.map((event) => event.data.chunk.argumentsDelta)
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
const data = { ...base, texts: run.map((event) => event.data.chunk.text) };
|
|
626
|
+
return kind === "text-delta" ? { type: "text-chunks", ...envelope, data } : { type: "reasoning-chunks", ...envelope, data };
|
|
627
|
+
}
|
|
628
|
+
function packChunkRuns(events) {
|
|
629
|
+
const out = [];
|
|
630
|
+
let kind;
|
|
631
|
+
let run = [];
|
|
632
|
+
const flush = () => {
|
|
633
|
+
if (kind !== void 0 && run.length >= MIN_RUN) out.push(buildRow(kind, run));
|
|
634
|
+
else out.push(...run);
|
|
635
|
+
kind = void 0;
|
|
636
|
+
run = [];
|
|
637
|
+
};
|
|
638
|
+
for (const event of events) {
|
|
639
|
+
const k = classify(event);
|
|
640
|
+
if (k === void 0) {
|
|
641
|
+
flush();
|
|
642
|
+
out.push(event);
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
const delta = event;
|
|
646
|
+
const last = run[run.length - 1];
|
|
647
|
+
if (k === kind && last !== void 0 && continues(last, delta, k)) {
|
|
648
|
+
run.push(delta);
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
flush();
|
|
652
|
+
kind = k;
|
|
653
|
+
run = [delta];
|
|
654
|
+
}
|
|
655
|
+
flush();
|
|
656
|
+
return out;
|
|
657
|
+
}
|
|
658
|
+
function malformed(tag, why) {
|
|
659
|
+
throw new Error(`malformed ${tag} storage row: ${why}`);
|
|
660
|
+
}
|
|
661
|
+
function validateRunData(tag, data, payloadKey) {
|
|
662
|
+
if (typeof data.turn !== "number" || typeof data.step !== "number" || typeof data.index !== "number") {
|
|
663
|
+
malformed(tag, "turn/step/index must be numbers");
|
|
664
|
+
}
|
|
665
|
+
const payload = data[payloadKey];
|
|
666
|
+
if (!Array.isArray(payload) || payload.length === 0 || payload.some((entry) => typeof entry !== "string")) {
|
|
667
|
+
malformed(tag, `${payloadKey} must be a non-empty string array`);
|
|
668
|
+
}
|
|
669
|
+
const dt = data.dt;
|
|
670
|
+
if (!Array.isArray(dt) || dt.some((gap) => !Number.isSafeInteger(gap))) {
|
|
671
|
+
malformed(tag, "dt must be an array of safe integers");
|
|
672
|
+
}
|
|
673
|
+
if (dt.length !== payload.length - 1) {
|
|
674
|
+
malformed(tag, `dt length ${dt.length} does not match ${payload.length} members`);
|
|
675
|
+
}
|
|
676
|
+
return payload;
|
|
677
|
+
}
|
|
678
|
+
function validateRow(value, tag) {
|
|
679
|
+
if (!hasExactKeys(value, ["type", "seq0", "time0", "data"])) {
|
|
680
|
+
malformed(tag, "envelope must be exactly {type, seq0, time0, data}");
|
|
681
|
+
}
|
|
682
|
+
if (!Number.isSafeInteger(value.seq0) || value.seq0 < 0) {
|
|
683
|
+
malformed(tag, "seq0 must be a non-negative safe integer");
|
|
684
|
+
}
|
|
685
|
+
if (!Number.isSafeInteger(value.time0)) {
|
|
686
|
+
malformed(tag, "time0 must be a safe integer");
|
|
687
|
+
}
|
|
688
|
+
const data = value.data;
|
|
689
|
+
if (!isRecord(data)) malformed(tag, "data must be an object");
|
|
690
|
+
let payload;
|
|
691
|
+
if (tag === "tool-call-chunks") {
|
|
692
|
+
const withName = hasExactKeys(data, ["turn", "step", "index", "id", "name", "dt", "args"]);
|
|
693
|
+
if (!withName && !hasExactKeys(data, ["turn", "step", "index", "id", "dt", "args"])) {
|
|
694
|
+
malformed(tag, "data must be exactly {turn, step, index, id, name?, dt, args}");
|
|
695
|
+
}
|
|
696
|
+
if (typeof data.id !== "string" || withName && typeof data.name !== "string") {
|
|
697
|
+
malformed(tag, "id (and name when present) must be strings");
|
|
698
|
+
}
|
|
699
|
+
payload = validateRunData(tag, data, "args");
|
|
700
|
+
} else {
|
|
701
|
+
if (!hasExactKeys(data, ["turn", "step", "index", "dt", "texts"])) {
|
|
702
|
+
malformed(tag, "data must be exactly {turn, step, index, dt, texts}");
|
|
703
|
+
}
|
|
704
|
+
payload = validateRunData(tag, data, "texts");
|
|
705
|
+
}
|
|
706
|
+
if (!Number.isSafeInteger(value.seq0 + payload.length - 1)) {
|
|
707
|
+
malformed(tag, "member seqs must stay safe integers");
|
|
708
|
+
}
|
|
709
|
+
let time = value.time0;
|
|
710
|
+
for (const gap of data.dt) {
|
|
711
|
+
time += gap;
|
|
712
|
+
if (!Number.isSafeInteger(time)) malformed(tag, "member times must stay safe integers");
|
|
713
|
+
}
|
|
714
|
+
return value;
|
|
715
|
+
}
|
|
716
|
+
function expandRow(row) {
|
|
717
|
+
const members = row.type === "tool-call-chunks" ? row.data.args : row.data.texts;
|
|
718
|
+
const events = [];
|
|
719
|
+
let time = row.time0;
|
|
720
|
+
for (let k = 0; k < members.length; k++) {
|
|
721
|
+
if (k > 0) time += row.data.dt[k - 1];
|
|
722
|
+
let chunk;
|
|
723
|
+
switch (row.type) {
|
|
724
|
+
case "text-chunks":
|
|
725
|
+
chunk = { type: "text-delta", index: row.data.index, text: members[k] };
|
|
726
|
+
break;
|
|
727
|
+
case "reasoning-chunks":
|
|
728
|
+
chunk = { type: "reasoning-delta", index: row.data.index, text: members[k] };
|
|
729
|
+
break;
|
|
730
|
+
case "tool-call-chunks":
|
|
731
|
+
chunk = {
|
|
732
|
+
type: "tool-call-delta",
|
|
733
|
+
index: row.data.index,
|
|
734
|
+
id: row.data.id,
|
|
735
|
+
...Object.hasOwn(row.data, "name") ? { name: row.data.name } : {},
|
|
736
|
+
argumentsDelta: members[k]
|
|
737
|
+
};
|
|
738
|
+
break;
|
|
739
|
+
/* v8 ignore next 2 -- validateRow only returns the three row tags */
|
|
740
|
+
default:
|
|
741
|
+
return assertNever(row, "chunk-rows expandRow");
|
|
742
|
+
}
|
|
743
|
+
events.push({
|
|
744
|
+
type: "assistant/chunk",
|
|
745
|
+
seq: row.seq0 + k,
|
|
746
|
+
time,
|
|
747
|
+
data: { turn: row.data.turn, step: row.data.step, chunk }
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
return events;
|
|
751
|
+
}
|
|
752
|
+
function decodeStorageRecord(value) {
|
|
753
|
+
if (!isRecord(value)) return [value];
|
|
754
|
+
const tag = value.type;
|
|
755
|
+
if (tag !== "text-chunks" && tag !== "reasoning-chunks" && tag !== "tool-call-chunks") {
|
|
756
|
+
return [value];
|
|
757
|
+
}
|
|
758
|
+
return expandRow(validateRow(value, tag));
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// packages/core/session/src/known-event-types.ts
|
|
762
|
+
var KNOWN_SESSION_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
763
|
+
"agent-preset/selected",
|
|
764
|
+
"agent/inbox/spliced",
|
|
765
|
+
"approval/asked",
|
|
766
|
+
"approval/decided",
|
|
767
|
+
"approval/policy",
|
|
768
|
+
"assistant/chunk",
|
|
769
|
+
"assistant/message",
|
|
770
|
+
"command/done",
|
|
771
|
+
"command/run",
|
|
772
|
+
"compaction/end",
|
|
773
|
+
"compaction/prune",
|
|
774
|
+
"compaction/start",
|
|
775
|
+
"compaction/summary",
|
|
776
|
+
"feedback/record",
|
|
777
|
+
"goal/change",
|
|
778
|
+
"hook/invoked",
|
|
779
|
+
"hook/result",
|
|
780
|
+
"llm/retry",
|
|
781
|
+
"llm/retry-started",
|
|
782
|
+
"permission/preset",
|
|
783
|
+
"plan/mode",
|
|
784
|
+
"request/context",
|
|
785
|
+
"request/header",
|
|
786
|
+
"sandbox/mode",
|
|
787
|
+
"schedule/change",
|
|
788
|
+
"session/end-seed",
|
|
789
|
+
"session/title",
|
|
790
|
+
"session/title-llm-request",
|
|
791
|
+
"step/end",
|
|
792
|
+
"step/start",
|
|
793
|
+
"subagent/descriptor",
|
|
794
|
+
"todo/write",
|
|
795
|
+
"tool-workflow/agent-end",
|
|
796
|
+
"tool-workflow/agent-start",
|
|
797
|
+
"tool-workflow/run-end",
|
|
798
|
+
"tool-workflow/run-start",
|
|
799
|
+
"tool/call",
|
|
800
|
+
"tool/code-dispatch",
|
|
801
|
+
"tool/code-dispatch-start",
|
|
802
|
+
"tool/result",
|
|
803
|
+
"turn/end",
|
|
804
|
+
"turn/start",
|
|
805
|
+
"user/message",
|
|
806
|
+
"web/deepseek-search-llm-request"
|
|
807
|
+
]);
|
|
808
|
+
|
|
809
|
+
// packages/core/session/src/index.ts
|
|
810
|
+
function validateSessionHeader(id, input) {
|
|
811
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
812
|
+
throw new Error("session header is not a plain JSON record");
|
|
813
|
+
}
|
|
814
|
+
const record = input;
|
|
815
|
+
if (record.version !== SESSION_FORMAT_VERSION) {
|
|
816
|
+
throw new Error(`session header version must be ${SESSION_FORMAT_VERSION}, got ${String(record.version)}`);
|
|
817
|
+
}
|
|
818
|
+
if (record.id !== id) {
|
|
819
|
+
throw new Error(`session header id "${String(record.id)}" does not match session id "${id}"`);
|
|
820
|
+
}
|
|
821
|
+
if (typeof record.createdAt !== "number" || !Number.isSafeInteger(record.createdAt) || record.createdAt < 0) {
|
|
822
|
+
throw new Error("session header createdAt must be a non-negative safe integer");
|
|
823
|
+
}
|
|
824
|
+
if (record.cwd !== void 0) {
|
|
825
|
+
if (typeof record.cwd !== "string") throw new Error("session header cwd must be a string");
|
|
826
|
+
if (!isAbsolute(record.cwd)) {
|
|
827
|
+
throw new Error(`session header cwd must be an absolute path, got "${record.cwd}"`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (record.parentSession !== void 0 && typeof record.parentSession !== "string") {
|
|
831
|
+
throw new Error("session header parentSession must be a string");
|
|
832
|
+
}
|
|
833
|
+
if (record.seedLength !== void 0 && (typeof record.seedLength !== "number" || !Number.isSafeInteger(record.seedLength) || record.seedLength < 0)) {
|
|
834
|
+
throw new Error("session header seedLength must be a non-negative safe integer");
|
|
835
|
+
}
|
|
836
|
+
if (record.origin !== void 0 && record.origin !== "subagent") {
|
|
837
|
+
throw new Error('session header origin must be "subagent"');
|
|
838
|
+
}
|
|
839
|
+
if (record.delegationDepth !== void 0 && (typeof record.delegationDepth !== "number" || !Number.isSafeInteger(record.delegationDepth) || record.delegationDepth < 0)) {
|
|
840
|
+
throw new Error("session header delegationDepth must be a non-negative safe integer");
|
|
841
|
+
}
|
|
842
|
+
if (record.agentPreset !== void 0 && typeof record.agentPreset !== "string") {
|
|
843
|
+
throw new Error("session header agentPreset must be a string");
|
|
844
|
+
}
|
|
845
|
+
return deepFreeze(record);
|
|
846
|
+
}
|
|
847
|
+
function validateRestoredSessionHeader(id, input) {
|
|
848
|
+
if (input !== null && typeof input === "object" && !Array.isArray(input)) {
|
|
849
|
+
const prototype = Reflect.getPrototypeOf(input);
|
|
850
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
851
|
+
throw new Error("session header is not a plain JSON record");
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return validateSessionHeader(id, input);
|
|
855
|
+
}
|
|
856
|
+
function snapshotSessionHeader(id, source) {
|
|
857
|
+
const input = source === void 0 ? { version: SESSION_FORMAT_VERSION, id, createdAt: Date.now() } : source;
|
|
858
|
+
const snapshot = snapshotJsonValue(input);
|
|
859
|
+
if (snapshot === void 0) throw new Error("session header is not losslessly JSON-serializable");
|
|
860
|
+
return validateSessionHeader(id, snapshot);
|
|
861
|
+
}
|
|
862
|
+
function adoptSessionEvent(event) {
|
|
863
|
+
assertMessageEventShape(
|
|
864
|
+
event,
|
|
865
|
+
`session event at seq ${event.seq}`
|
|
866
|
+
);
|
|
867
|
+
switch (event.type) {
|
|
868
|
+
case "user/message":
|
|
869
|
+
deepFreeze(event.data);
|
|
870
|
+
break;
|
|
871
|
+
case "assistant/message":
|
|
872
|
+
case "tool/result":
|
|
873
|
+
deepFreeze(event.data.message);
|
|
874
|
+
break;
|
|
875
|
+
default:
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
return event;
|
|
879
|
+
}
|
|
880
|
+
function snapshotSessionEvent(event) {
|
|
881
|
+
return adoptSessionEvent(structuredClone(event));
|
|
882
|
+
}
|
|
883
|
+
function freezeRestoredObject(value) {
|
|
884
|
+
const pending = [value];
|
|
885
|
+
while (pending.length > 0) {
|
|
886
|
+
const current = pending.pop();
|
|
887
|
+
Object.freeze(current);
|
|
888
|
+
for (const key in current) {
|
|
889
|
+
const child = current[key];
|
|
890
|
+
if (child !== null && typeof child === "object") pending.push(child);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
return value;
|
|
894
|
+
}
|
|
895
|
+
function assertSessionEventEnvelope(value, index) {
|
|
896
|
+
const event = value;
|
|
897
|
+
if (event["type"] === "request/header-delta") {
|
|
898
|
+
throw new Error(`seed event at index ${index} uses unsupported legacy request/header-delta format`);
|
|
899
|
+
}
|
|
900
|
+
for (const key in event) {
|
|
901
|
+
switch (key) {
|
|
902
|
+
case "type":
|
|
903
|
+
case "seq":
|
|
904
|
+
case "time":
|
|
905
|
+
case "data":
|
|
906
|
+
case "surfaceOp":
|
|
907
|
+
case "sourceEventSeqs":
|
|
908
|
+
case "ignorable":
|
|
909
|
+
break;
|
|
910
|
+
default:
|
|
911
|
+
throw new Error(`seed event at index ${index} has an invalid event envelope`);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
const type = event["type"];
|
|
915
|
+
const seq = event["seq"];
|
|
916
|
+
const time = event["time"];
|
|
917
|
+
if (typeof type !== "string" || typeof seq !== "number" || !Number.isSafeInteger(seq) || seq < 0 || typeof time !== "number" || !Number.isSafeInteger(time) || event["data"] === void 0 || event["ignorable"] !== void 0 && event["ignorable"] !== true) {
|
|
918
|
+
throw new Error(`seed event at index ${index} has an invalid event envelope`);
|
|
919
|
+
}
|
|
920
|
+
switch (type) {
|
|
921
|
+
case "request/header":
|
|
922
|
+
case "user/message":
|
|
923
|
+
case "assistant/message":
|
|
924
|
+
case "tool/result":
|
|
925
|
+
assertCurrentLlmShape(event, index);
|
|
926
|
+
break;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
function assertCurrentLlmShape(event, index) {
|
|
930
|
+
const data = event["data"];
|
|
931
|
+
const record = typeof data === "object" && data !== null ? data : void 0;
|
|
932
|
+
if (event["type"] === "request/header") {
|
|
933
|
+
const header = record?.["header"];
|
|
934
|
+
const headerRecord = typeof header === "object" && header !== null && !Array.isArray(header) ? header : void 0;
|
|
935
|
+
const config = headerRecord?.["config"];
|
|
936
|
+
if (!hasProviderModel(config)) throw new Error(`seed request/header at index ${index} lacks provider/model`);
|
|
937
|
+
const configRecord = config;
|
|
938
|
+
const reasoningEffort = configRecord["reasoningEffort"];
|
|
939
|
+
if (reasoningEffort !== void 0 && (typeof reasoningEffort !== "string" || reasoningEffort.length === 0)) {
|
|
940
|
+
throw new Error(`seed request/header at index ${index} has an invalid reasoningEffort`);
|
|
941
|
+
}
|
|
942
|
+
assertAdapterDefaults(headerRecord?.["adapterDefaults"], configRecord, index);
|
|
943
|
+
}
|
|
944
|
+
const type = event["type"];
|
|
945
|
+
if (type !== "user/message" && type !== "assistant/message" && type !== "tool/result") return;
|
|
946
|
+
assertMessageEventShape(event, `seed ${type} at index ${index}`);
|
|
947
|
+
}
|
|
948
|
+
var allowedAdapterKeys = /* @__PURE__ */ new Set(["reasoningEffort", "maxTokens"]);
|
|
949
|
+
function assertAdapterDefaults(value, config, index) {
|
|
950
|
+
if (value === void 0) return;
|
|
951
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
952
|
+
throw new Error(`seed request/header at index ${index} has invalid adapterDefaults`);
|
|
953
|
+
}
|
|
954
|
+
const defaults = value;
|
|
955
|
+
if (Object.keys(defaults).some((key) => !allowedAdapterKeys.has(key)) || Object.values(defaults).some((marker) => marker !== true) || defaults["reasoningEffort"] === true && config["reasoningEffort"] === void 0 || defaults["maxTokens"] === true && config["maxTokens"] === void 0) {
|
|
956
|
+
throw new Error(`seed request/header at index ${index} has invalid adapterDefaults`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
function assertMessageEventShape(event, subject) {
|
|
960
|
+
const type = event["type"];
|
|
961
|
+
if (type !== "user/message" && type !== "assistant/message" && type !== "tool/result") return;
|
|
962
|
+
const data = event["data"];
|
|
963
|
+
const record = typeof data === "object" && data !== null ? data : void 0;
|
|
964
|
+
const message = type === "user/message" ? record : record?.["message"];
|
|
965
|
+
if (typeof message !== "object" || message === null || typeof message["id"] !== "string" || message["id"] === "") {
|
|
966
|
+
throw new Error(`${subject} lacks an identified message`);
|
|
967
|
+
}
|
|
968
|
+
const messageRecord = message;
|
|
969
|
+
const expectedRole = type === "assistant/message" ? "assistant" : "user";
|
|
970
|
+
if (messageRecord["role"] !== expectedRole) {
|
|
971
|
+
throw new Error(`${subject} message must have role "${expectedRole}"`);
|
|
972
|
+
}
|
|
973
|
+
const source = messageRecord["source"];
|
|
974
|
+
if (typeof source !== "object" || source === null || typeof source["kind"] !== "string" || source["kind"] === "") {
|
|
975
|
+
throw new Error(`${subject} message has invalid source`);
|
|
976
|
+
}
|
|
977
|
+
if (!Array.isArray(messageRecord["content"])) {
|
|
978
|
+
throw new Error(`${subject} message has invalid content`);
|
|
979
|
+
}
|
|
980
|
+
const sourceRecord = source;
|
|
981
|
+
if (type === "assistant/message") {
|
|
982
|
+
if (sourceRecord["kind"] !== "model" || !hasProviderModel(sourceRecord)) {
|
|
983
|
+
throw new Error(`${subject} message must have model source`);
|
|
984
|
+
}
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
if (type !== "tool/result") return;
|
|
988
|
+
if (sourceRecord["kind"] !== "tool" || typeof sourceRecord["callId"] !== "string" || sourceRecord["callId"] === "") {
|
|
989
|
+
throw new Error(`${subject} message must have tool source`);
|
|
990
|
+
}
|
|
991
|
+
const content = messageRecord["content"];
|
|
992
|
+
const block = content[0];
|
|
993
|
+
if (content.length !== 1 || typeof block !== "object" || block === null || block["type"] !== "tool-result" || !Array.isArray(block["content"])) {
|
|
994
|
+
throw new Error(`${subject} message must contain one tool-result block`);
|
|
995
|
+
}
|
|
996
|
+
if (block["toolCallId"] !== sourceRecord["callId"]) {
|
|
997
|
+
throw new Error(`${subject} message has mismatched tool call ids`);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
function hasProviderModel(value) {
|
|
1001
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1002
|
+
const pair = value;
|
|
1003
|
+
return typeof pair["provider"] === "string" && pair["provider"].length > 0 && typeof pair["model"] === "string" && pair["model"].length > 0;
|
|
1004
|
+
}
|
|
1005
|
+
function assertSupportedRequestHeader(type, data, location) {
|
|
1006
|
+
if (type === "request/header-delta") {
|
|
1007
|
+
throw new Error(`${location} uses unsupported legacy request/header-delta format`);
|
|
1008
|
+
}
|
|
1009
|
+
if (type === "request/header" && data !== null && typeof data === "object" && !Array.isArray(data) && data["reason"] === "fallback") {
|
|
1010
|
+
throw new Error(`${location} uses unsupported legacy request/header reason "fallback"`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
function collectSessionCallbacks(ctx, args) {
|
|
1014
|
+
return [...ctx.events.dispatch("emit", args)];
|
|
1015
|
+
}
|
|
1016
|
+
function invokeContainedSessionObservers(ctx, name, id, args, callbacks) {
|
|
1017
|
+
for (const callback of callbacks) {
|
|
1018
|
+
try {
|
|
1019
|
+
const returned = callback(...args);
|
|
1020
|
+
void Promise.resolve(returned).catch((error) => {
|
|
1021
|
+
ctx.logger.warn(`session "${id}": ${name} listener rejected: ${String(error)}`);
|
|
1022
|
+
});
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
ctx.logger.warn(`session "${id}": ${name} listener threw: ${String(error)}`);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
var attachments = /* @__PURE__ */ new WeakMap();
|
|
1029
|
+
var Session = class _Session {
|
|
1030
|
+
log = [];
|
|
1031
|
+
/** Single incremental owner of surface acceptance and projection state. */
|
|
1032
|
+
surfaceManager = new SurfaceManager(this.log);
|
|
1033
|
+
/** The ordered surface over this session's event log. */
|
|
1034
|
+
get surface() {
|
|
1035
|
+
return this.surfaceManager;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Detached, deep-frozen creation metadata (format version, cwd, lineage,
|
|
1039
|
+
* seed boundary). Supplied by the store via `ctx.sessions.create()`. When a
|
|
1040
|
+
* `Session` is created without a store-owned header, a minimal header is
|
|
1041
|
+
* synthesized (stamped with the current {@link SESSION_FORMAT_VERSION}) so
|
|
1042
|
+
* `session.header` is always present. Kept out of the event log — it is a
|
|
1043
|
+
* storage concern, not replayable conversation state.
|
|
1044
|
+
*/
|
|
1045
|
+
header;
|
|
1046
|
+
/** The session identity, derived from its durable header's single copy. */
|
|
1047
|
+
get id() {
|
|
1048
|
+
return this.header.id;
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* The first seq appended IN THIS PROCESS: the length of the constructor
|
|
1052
|
+
* seed (0 without one). Events with smaller seq values entered through
|
|
1053
|
+
* construction — replay, fork, or resume — and were never published on the
|
|
1054
|
+
* `session/event` firehose (constructor seeds do not emit), so consumers
|
|
1055
|
+
* that replay the log as a publication substitute (telemetry adoption)
|
|
1056
|
+
* start here. Distinct from `header.seedLength`, the DURABLE fork-lineage
|
|
1057
|
+
* boundary: a resumed session's constructor seed is its full stored log,
|
|
1058
|
+
* while its header keeps the original fork value — this field is the
|
|
1059
|
+
* in-process construction fact.
|
|
1060
|
+
*
|
|
1061
|
+
* Not persisted itself: a seeded session projects it into the log as the
|
|
1062
|
+
* `session/end-seed` event, which is what a consumer reading STORED history
|
|
1063
|
+
* reads. Locate the LAST such event, not necessarily one at this seq — a
|
|
1064
|
+
* seed already ending in one is not re-marked, so reopening an untouched
|
|
1065
|
+
* session leaves that event at a smaller seq than `firstLiveSeq`. Prefer
|
|
1066
|
+
* this field in-process: it is exact before the marker reaches storage.
|
|
1067
|
+
*
|
|
1068
|
+
* When this lifecycle appends the marker, it occupies this seq before the
|
|
1069
|
+
* store attaches and therefore does not publish either. Otherwise this seq
|
|
1070
|
+
* holds an ordinary published write.
|
|
1071
|
+
*/
|
|
1072
|
+
firstLiveSeq;
|
|
1073
|
+
/**
|
|
1074
|
+
* Create a detached session by validating and snapshotting borrowed seed
|
|
1075
|
+
* events and storage metadata.
|
|
1076
|
+
* @param id - session identity.
|
|
1077
|
+
* @param seed - optional borrowed replay or fork events.
|
|
1078
|
+
* @param header - optional borrowed storage metadata.
|
|
1079
|
+
* @returns a detached session.
|
|
1080
|
+
*/
|
|
1081
|
+
static create(id, seed, header) {
|
|
1082
|
+
return new _Session(id, seed, header);
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Restore a detached session by taking ownership of fresh persistence values.
|
|
1086
|
+
* The storage format, event envelopes, sequence continuity, surface transitions,
|
|
1087
|
+
* and header fields are validated before the restored objects are frozen.
|
|
1088
|
+
* @param id - restored session identity.
|
|
1089
|
+
* @param seed - fresh detached events whose ownership is transferred.
|
|
1090
|
+
* @param header - fresh detached metadata whose ownership is transferred.
|
|
1091
|
+
* @returns a restored detached session.
|
|
1092
|
+
*/
|
|
1093
|
+
static fromRestore(id, seed, header) {
|
|
1094
|
+
return new _Session(id, seed, header, "restore");
|
|
1095
|
+
}
|
|
1096
|
+
constructor(id, seed, header, mode = "snapshot") {
|
|
1097
|
+
const restoredHeader = mode === "restore" ? validateRestoredSessionHeader(id, header) : void 0;
|
|
1098
|
+
if (seed !== void 0) {
|
|
1099
|
+
for (const [index, source] of seed.entries()) {
|
|
1100
|
+
const snapshot = mode === "restore" ? source : snapshotJsonValue(source);
|
|
1101
|
+
if (snapshot === void 0) {
|
|
1102
|
+
throw new Error(`seed event at index ${index} is not losslessly JSON-serializable`);
|
|
1103
|
+
}
|
|
1104
|
+
assertSessionEventEnvelope(snapshot, index);
|
|
1105
|
+
assertSupportedRequestHeader(snapshot.type, snapshot.data, `seed event at index ${index}`);
|
|
1106
|
+
if (snapshot.seq !== index) {
|
|
1107
|
+
throw new Error(`seed event at index ${index} has seq ${snapshot.seq} (expected ${index}); seed must be contiguous from 0`);
|
|
1108
|
+
}
|
|
1109
|
+
try {
|
|
1110
|
+
this.surfaceManager.validateNext(snapshot);
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
throw new Error(`invalid seed event at index ${index}: ${error instanceof Error ? error.message : "invalid surface metadata"}`);
|
|
1113
|
+
}
|
|
1114
|
+
this.log.push(mode === "restore" ? freezeRestoredObject(snapshot) : deepFreeze(snapshot));
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
this.firstLiveSeq = this.log.length;
|
|
1118
|
+
this.header = restoredHeader ?? snapshotSessionHeader(id, header);
|
|
1119
|
+
if (seed !== void 0 && this.log.at(-1)?.type !== "session/end-seed") {
|
|
1120
|
+
this.append("session/end-seed", {});
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
/** Cached immutable public snapshot of the private append-only log. */
|
|
1124
|
+
eventsSnapshot;
|
|
1125
|
+
/**
|
|
1126
|
+
* An immutable snapshot of the append-only event log. The snapshot is reused
|
|
1127
|
+
* until the next append; a previously returned array does not grow later.
|
|
1128
|
+
* Events and their nested data are deep-frozen at acceptance, so neither a
|
|
1129
|
+
* cast nor ordinary JavaScript can rewrite durable history.
|
|
1130
|
+
*/
|
|
1131
|
+
get events() {
|
|
1132
|
+
this.eventsSnapshot ??= Object.freeze([...this.log]);
|
|
1133
|
+
return this.eventsSnapshot;
|
|
1134
|
+
}
|
|
1135
|
+
/** The next event's sequence number — always the log length (the `seq = log.length` contiguity contract). */
|
|
1136
|
+
get seq() {
|
|
1137
|
+
return this.log.length;
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Append one typed event to the log and synchronously notify observers via
|
|
1141
|
+
* the store-owned, module-private publication hooks. The hot path never blocks
|
|
1142
|
+
* on I/O — persistence plugins buffer asynchronously. Once the event enters
|
|
1143
|
+
* the log, the append is committed: observer failures are logged and
|
|
1144
|
+
* contained per listener, so they do not change the return value or prevent
|
|
1145
|
+
* later listeners from observing the same accepted event.
|
|
1146
|
+
*
|
|
1147
|
+
* @param type - The event type (key of {@link SessionEventMap}).
|
|
1148
|
+
* @param data - The event payload; must be JSON-serializable.
|
|
1149
|
+
* @param opts - Surface metadata: `surfaceOp` controls how the event enters
|
|
1150
|
+
* the ordered surface; `sourceEventSeqs` lists the seq numbers of earlier
|
|
1151
|
+
* events this one derives from. REQUIRED for
|
|
1152
|
+
* {@link SurfaceEventType} events (every message-producing event must
|
|
1153
|
+
* declare how it joins the surface, the sole source of derived model
|
|
1154
|
+
* history) and
|
|
1155
|
+
* rejected by the compiler for non-surface types like `turn/start` or
|
|
1156
|
+
* `assistant/chunk`.
|
|
1157
|
+
* @returns the logged event — its assigned `seq`/`time` plus the SNAPSHOT of
|
|
1158
|
+
* `data` that entered the log, so reading `event.data` back sees the logged
|
|
1159
|
+
* value, never the caller's still-mutable input.
|
|
1160
|
+
* @throws if `data` or surface metadata is not losslessly JSON-serializable
|
|
1161
|
+
* (BigInt, function, symbol, undefined, negative zero, non-finite number,
|
|
1162
|
+
* circular reference, sparse array, or an exotic object such as
|
|
1163
|
+
* Map/Set/Date/class instance), or when the candidate violates the
|
|
1164
|
+
* canonical surface contract (marker shape and eligibility, unique
|
|
1165
|
+
* earlier source-event references, positional replacement validity, and complete
|
|
1166
|
+
* shadowed-node coverage). One recursive pass reads, validates, and
|
|
1167
|
+
* copies each nested value once, so a stateful getter cannot supply one value
|
|
1168
|
+
* to validation and another to storage. The event log is the durable source
|
|
1169
|
+
* of truth, so a bad event fails at the append site rather than later during
|
|
1170
|
+
* a backend flush. A synchronous internal dispatch validation failure or an
|
|
1171
|
+
* append reentered while this acceptance/publication boundary is open also
|
|
1172
|
+
* rejects before the log changes.
|
|
1173
|
+
*/
|
|
1174
|
+
append(type, data, ...opts) {
|
|
1175
|
+
const surfaceOpts = opts[0];
|
|
1176
|
+
const surfaceMetadata = {
|
|
1177
|
+
...surfaceOpts?.sourceEventSeqs === void 0 ? {} : { sourceEventSeqs: surfaceOpts.sourceEventSeqs },
|
|
1178
|
+
...surfaceOpts?.surfaceOp === void 0 ? {} : { surfaceOp: surfaceOpts.surfaceOp }
|
|
1179
|
+
};
|
|
1180
|
+
const dataSnapshot = snapshotJsonValue(data);
|
|
1181
|
+
if (dataSnapshot === void 0) {
|
|
1182
|
+
throw new Error(`session event "${type}" carries non-JSON-serializable data`);
|
|
1183
|
+
}
|
|
1184
|
+
assertSupportedRequestHeader(type, dataSnapshot, `session event "${type}"`);
|
|
1185
|
+
const surfaceMetadataSnapshot = snapshotJsonValue(surfaceMetadata);
|
|
1186
|
+
if (surfaceMetadataSnapshot === void 0) {
|
|
1187
|
+
throw new Error(`session event "${type}" carries non-JSON-serializable surface metadata`);
|
|
1188
|
+
}
|
|
1189
|
+
const entry = attachments.get(this);
|
|
1190
|
+
if (entry?.appending) {
|
|
1191
|
+
throw new Error("session append cannot reenter while another append is being published");
|
|
1192
|
+
}
|
|
1193
|
+
const event = deepFreeze({
|
|
1194
|
+
type,
|
|
1195
|
+
seq: this.log.length,
|
|
1196
|
+
time: Date.now(),
|
|
1197
|
+
data: dataSnapshot,
|
|
1198
|
+
...surfaceMetadataSnapshot
|
|
1199
|
+
});
|
|
1200
|
+
this.surfaceManager.validateNext(event);
|
|
1201
|
+
if (entry !== void 0) entry.appending = true;
|
|
1202
|
+
try {
|
|
1203
|
+
let callbacks;
|
|
1204
|
+
const callbackArgs = [this, event];
|
|
1205
|
+
if (entry !== void 0) {
|
|
1206
|
+
callbacks = collectSessionCallbacks(entry.emitCtx, [entry.carrier, "session/event", ...callbackArgs]);
|
|
1207
|
+
}
|
|
1208
|
+
this.log.push(event);
|
|
1209
|
+
this.eventsSnapshot = void 0;
|
|
1210
|
+
if (callbacks !== void 0 && entry !== void 0) {
|
|
1211
|
+
invokeContainedSessionObservers(entry.emitCtx, "session/event", entry.id, callbackArgs, callbacks);
|
|
1212
|
+
}
|
|
1213
|
+
return event;
|
|
1214
|
+
} finally {
|
|
1215
|
+
if (entry !== void 0) {
|
|
1216
|
+
entry.appending = false;
|
|
1217
|
+
if (entry.detachRequested && !entry.announcing) entry.detach();
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
/** Cached fold of the request-header events — see {@link requestHeader}. */
|
|
1222
|
+
headerFold;
|
|
1223
|
+
/** Log position (events consumed) the header fold has reached. */
|
|
1224
|
+
headerFoldSeq = 0;
|
|
1225
|
+
/**
|
|
1226
|
+
* The {@link EpochHeader} in force after the log's last header event — the
|
|
1227
|
+
* header the NEXT request will be compared against — or undefined before
|
|
1228
|
+
* the first `request/header` snapshot. The live, incrementally-maintained
|
|
1229
|
+
* form of `foldRequestHeader(session.events)`: each header event is folded
|
|
1230
|
+
* once, when first seen, so a per-step read costs O(new events).
|
|
1231
|
+
* @returns the folded header, or undefined when no header event exists yet.
|
|
1232
|
+
*/
|
|
1233
|
+
requestHeader() {
|
|
1234
|
+
if (this.headerFoldSeq < this.log.length) {
|
|
1235
|
+
this.headerFold = deepFreeze(foldRequestHeader(this.log.slice(this.headerFoldSeq), this.headerFold));
|
|
1236
|
+
this.headerFoldSeq = this.log.length;
|
|
1237
|
+
}
|
|
1238
|
+
return this.headerFold;
|
|
1239
|
+
}
|
|
1240
|
+
/** Cached fold of `request/context` events. */
|
|
1241
|
+
contextFold;
|
|
1242
|
+
contextFoldSeq = 0;
|
|
1243
|
+
/**
|
|
1244
|
+
* Return the latest resolved route metadata, or `undefined` before the first
|
|
1245
|
+
* `request/context` event. Each event is folded once.
|
|
1246
|
+
* @returns the latest immutable route metadata.
|
|
1247
|
+
*/
|
|
1248
|
+
requestContext() {
|
|
1249
|
+
if (this.contextFoldSeq < this.log.length) {
|
|
1250
|
+
for (const event of this.log.slice(this.contextFoldSeq)) {
|
|
1251
|
+
if (event.type === "request/context") this.contextFold = deepFreeze({ ...event.data });
|
|
1252
|
+
}
|
|
1253
|
+
this.contextFoldSeq = this.log.length;
|
|
1254
|
+
}
|
|
1255
|
+
return this.contextFold;
|
|
1256
|
+
}
|
|
1257
|
+
/** The derived-message cache: frozen projections, extended per unseen node. */
|
|
1258
|
+
derived = [];
|
|
1259
|
+
/** Surface position (nodes projected) the cache has reached. */
|
|
1260
|
+
derivedNodes = 0;
|
|
1261
|
+
/** {@link SurfaceManager.replaceGeneration} the cache was built under. */
|
|
1262
|
+
derivedGeneration = 0;
|
|
1263
|
+
/**
|
|
1264
|
+
* Derive the LLM message history by walking the ordered sequences of
|
|
1265
|
+
* message-producing events maintained by `surfaceOp` markers. The
|
|
1266
|
+
* surface is the single source of derived history: every message-producing
|
|
1267
|
+
* append records its `surfaceOp`, so a raw event with no marker (a chunk, a
|
|
1268
|
+
* turn boundary) is correctly absent, and a compaction `replace` deletes the
|
|
1269
|
+
* shadowed nodes from the derivation. The projection rules are
|
|
1270
|
+
* {@link deriveEventMessage}, folded per node.
|
|
1271
|
+
*
|
|
1272
|
+
* CACHED: each surface node is projected exactly once, when first seen — a
|
|
1273
|
+
* call costs O(new nodes), and a surface rewrite (a `replace`;
|
|
1274
|
+
* {@link SessionSurface.replaceGeneration}) rebuilds. The returned array is
|
|
1275
|
+
* a fresh snapshot per call (later appends never grow an array a caller
|
|
1276
|
+
* already holds); the `Message` objects in it are SHARED and **deep-frozen**.
|
|
1277
|
+
* Their content reuses the already frozen durable event data, so the cache
|
|
1278
|
+
* needs no second deep clone and consumers still cannot mutate the log.
|
|
1279
|
+
* @returns a fresh array of the shared, frozen derived history.
|
|
1280
|
+
*/
|
|
1281
|
+
deriveMessages() {
|
|
1282
|
+
const surface = this.surface;
|
|
1283
|
+
const nodes = surface.nodes;
|
|
1284
|
+
const generation = surface.replaceGeneration;
|
|
1285
|
+
if (generation !== this.derivedGeneration) {
|
|
1286
|
+
this.derived = [];
|
|
1287
|
+
this.derivedNodes = 0;
|
|
1288
|
+
this.derivedGeneration = generation;
|
|
1289
|
+
}
|
|
1290
|
+
for (const seq of nodes.slice(this.derivedNodes)) {
|
|
1291
|
+
const msg = this.deriveEventMessage(this.log[seq]);
|
|
1292
|
+
if (msg) this.derived.push(msg);
|
|
1293
|
+
}
|
|
1294
|
+
this.derivedNodes = nodes.length;
|
|
1295
|
+
return [...this.derived];
|
|
1296
|
+
}
|
|
1297
|
+
/**
|
|
1298
|
+
* Instance face of the pure per-node `deriveEventMessage` export from
|
|
1299
|
+
* `surface.ts`.
|
|
1300
|
+
* @param event - the event to project.
|
|
1301
|
+
* @returns the derived message, or null when the event produces none.
|
|
1302
|
+
*/
|
|
1303
|
+
deriveEventMessage(event) {
|
|
1304
|
+
return deriveEventMessage(event);
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
var SessionForkError = class extends Error {
|
|
1308
|
+
constructor(message, code) {
|
|
1309
|
+
super(message);
|
|
1310
|
+
this.code = code;
|
|
1311
|
+
this.name = "SessionForkError";
|
|
1312
|
+
}
|
|
1313
|
+
code;
|
|
1314
|
+
};
|
|
1315
|
+
var SessionStore = class extends Service {
|
|
1316
|
+
store = /* @__PURE__ */ new Map();
|
|
1317
|
+
counter = 0;
|
|
1318
|
+
constructor(ctx) {
|
|
1319
|
+
super(ctx, "sessions");
|
|
1320
|
+
ctx.inject(["typert"], (typeCtx) => {
|
|
1321
|
+
typeCtx.typert.lookups.register("session", {
|
|
1322
|
+
parameter: "session",
|
|
1323
|
+
wire: "sessionId",
|
|
1324
|
+
hostTypeSymbol: "@deepseek-ai/dsh-session#Session",
|
|
1325
|
+
wireTypeSymbol: "@deepseek-ai/dsh-session/types#SessionId",
|
|
1326
|
+
resolve: (sessionId) => this.get(sessionId)
|
|
1327
|
+
});
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Create a session owned by the calling fiber: disposing that fiber stops
|
|
1332
|
+
* event notification and removes the session from the store. `options.seed`
|
|
1333
|
+
* populates the session with a copy of those events (replay/fork);
|
|
1334
|
+
* `options.meta` attaches creation metadata (validated absolute `cwd`, seed
|
|
1335
|
+
* and parent lineage, and delegation depth) as the immutable
|
|
1336
|
+
* {@link SessionHeader} (the store fills `version`/`id`/`createdAt`).
|
|
1337
|
+
*
|
|
1338
|
+
* For an agent whose session must be torn down IN ORDER with its loop (so the
|
|
1339
|
+
* loop's final events are published before the store attachment ends), do NOT use this
|
|
1340
|
+
* — fold the session lifecycle into the agent's own effect via
|
|
1341
|
+
* {@link prepare} + {@link enter} + {@link announce} (see
|
|
1342
|
+
* `dsh-agent-loop`'s creation transaction).
|
|
1343
|
+
*
|
|
1344
|
+
* @param id - the session id; omitted, the store mints `session-<n>`.
|
|
1345
|
+
* @param options - seed events and/or creation metadata for the header.
|
|
1346
|
+
* @returns the live session, already entered and announced.
|
|
1347
|
+
* @throws if a session with `id` already exists, metadata is not a plain
|
|
1348
|
+
* lossless-JSON record with valid scalar fields, or `meta.cwd` is a
|
|
1349
|
+
* non-absolute path (storage backends key directories off it).
|
|
1350
|
+
*/
|
|
1351
|
+
create(id, options) {
|
|
1352
|
+
const session = this.prepare(id, options);
|
|
1353
|
+
this.ctx.effect(function* () {
|
|
1354
|
+
yield this.enter(session);
|
|
1355
|
+
this.announce(session);
|
|
1356
|
+
}.bind(this), "sessions.create()");
|
|
1357
|
+
return session;
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Build a session WITHOUT entering it into the store — validate the id/cwd and
|
|
1361
|
+
* construct the {@link Session} (with its immutable {@link SessionHeader}).
|
|
1362
|
+
* Pairs with {@link enter} + {@link announce}: a caller that owns a composite
|
|
1363
|
+
* `ctx.effect` (the agent factory) folds the session lifecycle into that ONE
|
|
1364
|
+
* effect so a fiber unload tears the session + agent down as a single ORDERED
|
|
1365
|
+
* chain rather than as racing sibling effects — which would remove the publication hooks
|
|
1366
|
+
* before the driver's closing events commit, dropping them.
|
|
1367
|
+
*
|
|
1368
|
+
* @param id - the session id; omitted, the store mints `session-<n>`.
|
|
1369
|
+
* @param options - seed events and/or creation metadata for the header. With
|
|
1370
|
+
* `seedSource: 'persistence'`, metadata and events must be fresh detached
|
|
1371
|
+
* graphs whose ownership transfers to this call: they are validated and
|
|
1372
|
+
* frozen in place through {@link Session.fromRestore}, so the caller must
|
|
1373
|
+
* retain no mutable aliases.
|
|
1374
|
+
* @returns the constructed session, NOT yet in the store.
|
|
1375
|
+
* @throws if a session with `id` already exists, metadata is not a plain
|
|
1376
|
+
* lossless-JSON record with valid scalar fields, or `meta.cwd` is a
|
|
1377
|
+
* non-absolute path.
|
|
1378
|
+
*/
|
|
1379
|
+
prepare(id, options) {
|
|
1380
|
+
let sessionId;
|
|
1381
|
+
if (id === void 0) {
|
|
1382
|
+
do
|
|
1383
|
+
sessionId = SessionId(`session-${++this.counter}`);
|
|
1384
|
+
while (this.store.has(sessionId));
|
|
1385
|
+
} else {
|
|
1386
|
+
sessionId = SessionId(id);
|
|
1387
|
+
}
|
|
1388
|
+
if (this.store.has(sessionId)) throw new Error(`session "${sessionId}" already exists`);
|
|
1389
|
+
if (options?.seedSource === "persistence") {
|
|
1390
|
+
return Session.fromRestore(sessionId, options.seed, options.meta);
|
|
1391
|
+
}
|
|
1392
|
+
const seed = options?.seed;
|
|
1393
|
+
const meta = options?.meta;
|
|
1394
|
+
const header = {
|
|
1395
|
+
version: SESSION_FORMAT_VERSION,
|
|
1396
|
+
id: sessionId,
|
|
1397
|
+
createdAt: meta?.createdAt ?? Date.now(),
|
|
1398
|
+
...meta?.cwd === void 0 ? {} : { cwd: meta.cwd },
|
|
1399
|
+
...meta?.parentSession === void 0 ? {} : { parentSession: meta.parentSession },
|
|
1400
|
+
...meta?.seedLength === void 0 ? {} : { seedLength: meta.seedLength },
|
|
1401
|
+
...meta?.origin === void 0 ? {} : { origin: meta.origin },
|
|
1402
|
+
...meta?.delegationDepth === void 0 ? {} : { delegationDepth: meta.delegationDepth },
|
|
1403
|
+
...meta?.agentPreset === void 0 ? {} : { agentPreset: meta.agentPreset }
|
|
1404
|
+
};
|
|
1405
|
+
return Session.create(sessionId, seed, header);
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Enter a {@link prepare}d session into the store: install the module-private
|
|
1409
|
+
* append publication hooks and add it to the store. Returns the DETACH
|
|
1410
|
+
* disposer (hooks + store removal). Does NOT emit `session/created` —
|
|
1411
|
+
* the caller yields this disposer inside its effect and THEN calls
|
|
1412
|
+
* {@link announce}, so a throwing `session/created` listener rolls the attach
|
|
1413
|
+
* back instead of leaking it.
|
|
1414
|
+
*
|
|
1415
|
+
* Re-checks the id for a duplicate: `prepare` and `enter` are public
|
|
1416
|
+
* cross-package primitives and a caller may interleave arbitrary work (or
|
|
1417
|
+
* another create) between them, so a stale prepared session must NOT overwrite
|
|
1418
|
+
* a live store entry of the same id — its detach disposer would later delete
|
|
1419
|
+
* the REAL session. The {@link create} convenience and the agent factory call
|
|
1420
|
+
* the two back-to-back so they never trip this, but the public API cannot
|
|
1421
|
+
* assume that.
|
|
1422
|
+
*
|
|
1423
|
+
* @param session - a {@link prepare}d session not yet in the store.
|
|
1424
|
+
* @returns the detach disposer (publication hooks + store removal). When called from
|
|
1425
|
+
* a synchronous `session/created` listener, removal and disposal wait until
|
|
1426
|
+
* that creation dispatch unwinds.
|
|
1427
|
+
* @throws if a session with this id is already in the store.
|
|
1428
|
+
*/
|
|
1429
|
+
enter(session) {
|
|
1430
|
+
const id = session.id;
|
|
1431
|
+
const carrier = scopeTarget(session, scopeOf(this.ctx));
|
|
1432
|
+
if (this.store.has(id)) throw new Error(`session "${id}" already exists`);
|
|
1433
|
+
if (attachments.has(session)) throw new Error(`session "${id}" is already attached to a store`);
|
|
1434
|
+
const entry = {
|
|
1435
|
+
id,
|
|
1436
|
+
session,
|
|
1437
|
+
carrier,
|
|
1438
|
+
emitCtx: this.ctx,
|
|
1439
|
+
announced: false,
|
|
1440
|
+
announcing: false,
|
|
1441
|
+
appending: false,
|
|
1442
|
+
detachRequested: false,
|
|
1443
|
+
detach: () => {
|
|
1444
|
+
this.detachEntered(entry);
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1447
|
+
this.store.set(id, entry);
|
|
1448
|
+
attachments.set(session, entry);
|
|
1449
|
+
let entered = true;
|
|
1450
|
+
const detach = () => {
|
|
1451
|
+
if (!entered) return;
|
|
1452
|
+
entered = false;
|
|
1453
|
+
if (entry.announcing || entry.appending) {
|
|
1454
|
+
entry.detachRequested = true;
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
entry.detach();
|
|
1458
|
+
};
|
|
1459
|
+
return detach;
|
|
1460
|
+
}
|
|
1461
|
+
/** Remove one exact entered session and emit its paired disposal when announced. */
|
|
1462
|
+
detachEntered(entry) {
|
|
1463
|
+
entry.detachRequested = false;
|
|
1464
|
+
if (this.store.get(entry.id) !== entry) return;
|
|
1465
|
+
this.store.delete(entry.id);
|
|
1466
|
+
attachments.delete(entry.session);
|
|
1467
|
+
if (entry.announced) this.emitDisposed(entry);
|
|
1468
|
+
}
|
|
1469
|
+
/** Emit `session/created` exactly once for an {@link enter}ed session (with
|
|
1470
|
+
* the carrier {@link enter} captured). Separate from {@link enter} so the
|
|
1471
|
+
* caller can yield the detach disposer first (rollback safety — see
|
|
1472
|
+
* {@link enter}).
|
|
1473
|
+
* @param session - the entered session to announce to listeners.
|
|
1474
|
+
* @throws if the session is not live or its announcement already began,
|
|
1475
|
+
* including a reentrant call from a creation listener. */
|
|
1476
|
+
announce(session) {
|
|
1477
|
+
const entry = this.liveEntryFor(session);
|
|
1478
|
+
if (entry.announced || entry.announcing) {
|
|
1479
|
+
throw new Error(`session "${entry.id}" was already announced`);
|
|
1480
|
+
}
|
|
1481
|
+
entry.announced = true;
|
|
1482
|
+
const callbackArgs = [session];
|
|
1483
|
+
entry.announcing = true;
|
|
1484
|
+
try {
|
|
1485
|
+
const callbacks = collectSessionCallbacks(this.ctx, [entry.carrier, "session/created", session]);
|
|
1486
|
+
for (const callback of callbacks) {
|
|
1487
|
+
const returned = callback(...callbackArgs);
|
|
1488
|
+
void Promise.resolve(returned).catch((error) => {
|
|
1489
|
+
this.ctx.logger.warn(`session "${entry.id}": session/created listener rejected: ${String(error)}`);
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
} finally {
|
|
1493
|
+
entry.announcing = false;
|
|
1494
|
+
if (entry.detachRequested && !entry.appending) entry.detach();
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
/** Emit the paired teardown notification with per-listener containment. */
|
|
1498
|
+
emitDisposed(entry) {
|
|
1499
|
+
const callbackArgs = [entry.session];
|
|
1500
|
+
try {
|
|
1501
|
+
const callbacks = collectSessionCallbacks(this.ctx, [entry.carrier, "session/disposed", entry.session]);
|
|
1502
|
+
invokeContainedSessionObservers(this.ctx, "session/disposed", entry.id, callbackArgs, callbacks);
|
|
1503
|
+
} catch (error) {
|
|
1504
|
+
this.ctx.logger.warn(`session "${entry.id}": session/disposed dispatch threw: ${String(error)}`);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Dispatch the awaited `session/flush` durability checkpoint for `session`,
|
|
1509
|
+
* with the carrier captured at {@link enter}. THE flush entry point: the
|
|
1510
|
+
* store owns the carrier, so callers (the checkpoint policy's per-request
|
|
1511
|
+
* barrier, goal-round-driver's idle checkpoint, teardown drains, and consumers
|
|
1512
|
+
* that flush themselves before reading storage) must come through here
|
|
1513
|
+
* rather than dispatch a raw `ctx.parallel('session/flush', …)` — one owner,
|
|
1514
|
+
* one spelling, and the scoped-dispatch invariant can pin it.
|
|
1515
|
+
* @param session - the session whose buffered events must reach durable storage.
|
|
1516
|
+
* @returns whether at least one durability listener participated, after every
|
|
1517
|
+
* listener has settled successfully.
|
|
1518
|
+
* @throws the first registered listener failure after every listener settles.
|
|
1519
|
+
*/
|
|
1520
|
+
async flush(session) {
|
|
1521
|
+
const { carrier } = this.liveEntryFor(session);
|
|
1522
|
+
const callbackArgs = [session];
|
|
1523
|
+
const callbacks = collectSessionCallbacks(this.ctx, [carrier, "session/flush", session]);
|
|
1524
|
+
const results = await Promise.allSettled(callbacks.map((callback) => {
|
|
1525
|
+
try {
|
|
1526
|
+
return callback(...callbackArgs);
|
|
1527
|
+
} catch (error) {
|
|
1528
|
+
return Promise.reject(error);
|
|
1529
|
+
}
|
|
1530
|
+
}));
|
|
1531
|
+
const failure = results.find((result) => result.status === "rejected");
|
|
1532
|
+
if (failure !== void 0) throw failure.reason;
|
|
1533
|
+
return callbacks.length > 0;
|
|
1534
|
+
}
|
|
1535
|
+
/** Return the exact live entry; detached/prepared objects reject. */
|
|
1536
|
+
liveEntryFor(session) {
|
|
1537
|
+
const entry = attachments.get(session);
|
|
1538
|
+
if (entry === void 0 || this.store.get(entry.id) !== entry) {
|
|
1539
|
+
throw new Error(`session "${session.id}" is not live in this store`);
|
|
1540
|
+
}
|
|
1541
|
+
return entry;
|
|
1542
|
+
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Look up a live session.
|
|
1545
|
+
* @param id - the session id to look up.
|
|
1546
|
+
* @returns the session, or undefined when no live session has that id.
|
|
1547
|
+
*/
|
|
1548
|
+
get(id) {
|
|
1549
|
+
return this.store.get(id)?.session;
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* All live sessions, in creation order.
|
|
1553
|
+
* @returns a fresh array; mutating it does not affect the store.
|
|
1554
|
+
*/
|
|
1555
|
+
list() {
|
|
1556
|
+
return [...this.store.values()].map((entry) => entry.session);
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Create a live child session from a stable prefix of a live source.
|
|
1560
|
+
* `boundary` is an inclusive source event seq; omitted means the source's
|
|
1561
|
+
* current last event. The selected slice may end with a between-turn event
|
|
1562
|
+
* but must not end inside an open turn.
|
|
1563
|
+
*
|
|
1564
|
+
* @param source - Live source session object or id.
|
|
1565
|
+
* @param boundary - Inclusive source event seq to fork through; omitted means
|
|
1566
|
+
* the source's current last event, and omitted on an empty source forks an
|
|
1567
|
+
* empty child.
|
|
1568
|
+
* @param childSessionId - Optional child session id; omitted delegates to
|
|
1569
|
+
* `SessionStore`'s id policy.
|
|
1570
|
+
* @returns The created live child session.
|
|
1571
|
+
*/
|
|
1572
|
+
fork(source, boundary, childSessionId) {
|
|
1573
|
+
if (childSessionId !== void 0 && this.get(childSessionId) !== void 0) {
|
|
1574
|
+
throw new SessionForkError(`session "${childSessionId}" already exists`, "SESSION_ALREADY_EXISTS");
|
|
1575
|
+
}
|
|
1576
|
+
const liveSource = this._resolveForkSource(source);
|
|
1577
|
+
const seed = this._forkSeed(liveSource, boundary);
|
|
1578
|
+
return this.create(childSessionId, {
|
|
1579
|
+
seed,
|
|
1580
|
+
meta: {
|
|
1581
|
+
...liveSource.header.cwd !== void 0 ? { cwd: liveSource.header.cwd } : {},
|
|
1582
|
+
parentSession: liveSource.id,
|
|
1583
|
+
seedLength: seed.length
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
_forkSeed(session, requestedBoundary) {
|
|
1588
|
+
const events = session.events;
|
|
1589
|
+
const lastEvent = events.at(-1);
|
|
1590
|
+
let boundary;
|
|
1591
|
+
if (requestedBoundary !== void 0) {
|
|
1592
|
+
boundary = requestedBoundary;
|
|
1593
|
+
} else {
|
|
1594
|
+
if (lastEvent === void 0) return [];
|
|
1595
|
+
boundary = lastEvent.seq;
|
|
1596
|
+
}
|
|
1597
|
+
if (!Number.isSafeInteger(boundary) || boundary < 0) {
|
|
1598
|
+
throw new SessionForkError(
|
|
1599
|
+
`fork boundary for session "${session.id}" must be a non-negative safe integer, got ${String(boundary)}`,
|
|
1600
|
+
"INVALID_BOUNDARY"
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
if (boundary >= events.length) {
|
|
1604
|
+
const lastSeq = events.at(-1)?.seq;
|
|
1605
|
+
throw new SessionForkError(
|
|
1606
|
+
`fork boundary ${boundary} does not exist in session "${session.id}" (last seq: ${lastSeq ?? "none"})`,
|
|
1607
|
+
"INVALID_BOUNDARY"
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
const boundaryEvent = events[boundary];
|
|
1611
|
+
if (boundaryEvent === void 0 || boundaryEvent.seq !== boundary) {
|
|
1612
|
+
throw new SessionForkError(
|
|
1613
|
+
`fork boundary ${boundary} does not match a contiguous event seq in session "${session.id}"`,
|
|
1614
|
+
"INVALID_BOUNDARY"
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1617
|
+
const lastTurnBoundary = events.slice(0, boundary + 1).findLast((event) => event.type === "turn/start" || event.type === "turn/end");
|
|
1618
|
+
if (lastTurnBoundary?.type === "turn/start") {
|
|
1619
|
+
throw new SessionForkError(
|
|
1620
|
+
`fork boundary ${boundary} in session "${session.id}" ends inside open turn ${lastTurnBoundary.data.turn}`,
|
|
1621
|
+
"OPEN_TURN"
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
return events.slice(0, boundary + 1);
|
|
1625
|
+
}
|
|
1626
|
+
_resolveForkSource(source) {
|
|
1627
|
+
if (typeof source === "string") {
|
|
1628
|
+
const session = this.get(source);
|
|
1629
|
+
if (session === void 0) throw new SessionForkError(`session "${source}" not found`, "SESSION_NOT_FOUND");
|
|
1630
|
+
return session;
|
|
1631
|
+
}
|
|
1632
|
+
const live = this.get(source.id);
|
|
1633
|
+
if (live === void 0) {
|
|
1634
|
+
throw new SessionForkError(`session "${source.id}" not found`, "SESSION_NOT_FOUND");
|
|
1635
|
+
}
|
|
1636
|
+
if (live !== source) throw new SessionForkError(`session "${source.id}" is not the live store instance`, "SESSION_NOT_LIVE");
|
|
1637
|
+
return source;
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
var index_default = SessionStore;
|
|
1641
|
+
export {
|
|
1642
|
+
KNOWN_SESSION_EVENT_TYPES,
|
|
1643
|
+
SESSION_FORMAT_VERSION,
|
|
1644
|
+
Session,
|
|
1645
|
+
SessionForkError,
|
|
1646
|
+
SessionId,
|
|
1647
|
+
SessionPreparation,
|
|
1648
|
+
SessionStore,
|
|
1649
|
+
TOOL_NOT_STARTED,
|
|
1650
|
+
TOOL_OUTCOME_UNKNOWN,
|
|
1651
|
+
adoptSessionEvent,
|
|
1652
|
+
canonicalHeader,
|
|
1653
|
+
decodeStorageRecord,
|
|
1654
|
+
index_default as default,
|
|
1655
|
+
deriveEventMessage,
|
|
1656
|
+
foldRequestHeader,
|
|
1657
|
+
foldSurface,
|
|
1658
|
+
headerEquals,
|
|
1659
|
+
interruptedTurnClosers,
|
|
1660
|
+
isAppendSurfaceEvent,
|
|
1661
|
+
isJsonValue,
|
|
1662
|
+
isReplacementSurfaceEvent,
|
|
1663
|
+
isSurfaceEligibleType,
|
|
1664
|
+
isSurfaceEvent,
|
|
1665
|
+
packChunkRuns,
|
|
1666
|
+
snapshotJsonValue,
|
|
1667
|
+
snapshotSessionEvent
|
|
1668
|
+
};
|