@deepseek-ai/dsh-session-format-v3-to-v4 0.1.7-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +350 -0
- package/README.zh.md +350 -0
- package/lib/index.js +1552 -0
- package/lib/types/codec.d.ts +48 -0
- package/lib/types/content.d.ts +17 -0
- package/lib/types/developer.d.ts +10 -0
- package/lib/types/extension-identities.d.ts +11 -0
- package/lib/types/facts.d.ts +35 -0
- package/lib/types/fork-result.d.ts +8 -0
- package/lib/types/index.d.ts +8 -0
- package/lib/types/message-sources.d.ts +13 -0
- package/lib/types/migration.d.ts +11 -0
- package/lib/types/references.d.ts +11 -0
- package/lib/types/relationships.d.ts +9 -0
- package/lib/types/retired-syntax.d.ts +8 -0
- package/lib/types/sources.d.ts +26 -0
- package/lib/types/system-message.d.ts +7 -0
- package/lib/types/testing/validation.d.ts +7 -0
- package/lib/types/tool-role.d.ts +19 -0
- package/lib/types/validation.d.ts +33 -0
- package/package.json +51 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1552 @@
|
|
|
1
|
+
import { assertReleasedV3Header, releasedV2SessionFormatCodec, releasedV3SessionFormatCodec } from "@deepseek-ai/dsh-session-format-v2-to-v3";
|
|
2
|
+
import { SessionFormatError, SessionFormatUnsupportedMigrationError, defineSessionFormatMigration, isSessionFormatJsonObject, sessionFormatCount } from "@deepseek-ai/dsh-session-format";
|
|
3
|
+
import { isAbsolute } from "node:path";
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
5
|
+
//#region lib/types/sources.js
|
|
6
|
+
/** Released V3 plugin-source conversion and declared message traversal. */
|
|
7
|
+
/**
|
|
8
|
+
* Visit only messages carried by first-party event payloads.
|
|
9
|
+
* @param event - source or target logical event.
|
|
10
|
+
* @param transform - message transform preserving unchanged identity.
|
|
11
|
+
* @returns the event sharing all untouched payloads.
|
|
12
|
+
*/
|
|
13
|
+
function mapEventMessages(event, transform) {
|
|
14
|
+
const data = event.data;
|
|
15
|
+
if (!isSessionFormatJsonObject(data)) return event;
|
|
16
|
+
if (event.type === "user/message") {
|
|
17
|
+
const message = transform(data);
|
|
18
|
+
return message === data ? event : {
|
|
19
|
+
...event,
|
|
20
|
+
data: message
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (event.type === "developer/message" || event.type === "system/message" || event.type === "assistant/message" || event.type === "tool/result") {
|
|
24
|
+
if (!isSessionFormatJsonObject(data["message"])) throw new SessionFormatError(event.type + " requires a message");
|
|
25
|
+
const message = transform(data["message"]);
|
|
26
|
+
return message === data["message"] ? event : {
|
|
27
|
+
...event,
|
|
28
|
+
data: {
|
|
29
|
+
...data,
|
|
30
|
+
message
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const key = event.type === "agent/inbox/spliced" ? "inserted" : event.type === "session/title-llm-request" ? "messages" : void 0;
|
|
35
|
+
if (key === void 0) return event;
|
|
36
|
+
const messages = data[key];
|
|
37
|
+
if (!Array.isArray(messages)) throw new SessionFormatError(event.type + " requires message array");
|
|
38
|
+
const mapped = messages.map((message) => {
|
|
39
|
+
if (!isSessionFormatJsonObject(message)) throw new SessionFormatError(event.type + " requires message objects");
|
|
40
|
+
return transform(message);
|
|
41
|
+
});
|
|
42
|
+
return mapped.every((message, index) => message === messages[index]) ? event : {
|
|
43
|
+
...event,
|
|
44
|
+
data: {
|
|
45
|
+
...data,
|
|
46
|
+
[key]: mapped
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Released V3 plugin names whose current producer kind is not the plugin string. */
|
|
51
|
+
const RENAMED_PRODUCERS = Object.freeze({
|
|
52
|
+
"compact": "compact-checkpoint",
|
|
53
|
+
"tools-code-mode": "ptc-mode",
|
|
54
|
+
"tools-ptc": "ptc-mode",
|
|
55
|
+
"dsh-compaction-basic": "compact-basic",
|
|
56
|
+
"@deepseek-ai/dsh-system-prompt": "runtime-context"
|
|
57
|
+
});
|
|
58
|
+
/** First-party V3 plugin identities that intentionally keep their current kind. */
|
|
59
|
+
const RELEASED_SAME_NAME_PRODUCERS = new Set([
|
|
60
|
+
"agent-instructions",
|
|
61
|
+
"session-reference",
|
|
62
|
+
"team-message",
|
|
63
|
+
"goal",
|
|
64
|
+
"skill-invocation",
|
|
65
|
+
"skill-catalog",
|
|
66
|
+
"coordinator",
|
|
67
|
+
"subagent-report",
|
|
68
|
+
"subagent-settled",
|
|
69
|
+
"webhook",
|
|
70
|
+
"agent-message",
|
|
71
|
+
"model-selection",
|
|
72
|
+
"plan-mode",
|
|
73
|
+
"time-context",
|
|
74
|
+
"tmux-context",
|
|
75
|
+
"user-approval",
|
|
76
|
+
"repeat-tool-reminder",
|
|
77
|
+
"tool-cordis",
|
|
78
|
+
"cordis-host-runner",
|
|
79
|
+
"tool-goal",
|
|
80
|
+
"tool-jobs",
|
|
81
|
+
"hooks-codex",
|
|
82
|
+
"hooks-claude-code",
|
|
83
|
+
"schedule",
|
|
84
|
+
"dsh-session-title-llm"
|
|
85
|
+
]);
|
|
86
|
+
/** Resolve the current producer kind for one released V3 plugin string. */
|
|
87
|
+
function producerKind(plugin, role) {
|
|
88
|
+
if (plugin === "@deepseek-ai/dsh-system-prompt" && role === "system") return "system-prompt";
|
|
89
|
+
const renamed = Object.hasOwn(RENAMED_PRODUCERS, plugin) ? RENAMED_PRODUCERS[plugin] : void 0;
|
|
90
|
+
if (renamed !== void 0) return renamed;
|
|
91
|
+
if (RELEASED_SAME_NAME_PRODUCERS.has(plugin)) return plugin;
|
|
92
|
+
return `plugin:${plugin}`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Lift one released V3 plugin source into the current producer-owned shape.
|
|
96
|
+
* @param source - source record whose `kind` is `'plugin'`.
|
|
97
|
+
* @param seq - event seq for diagnostics.
|
|
98
|
+
* @param role - carried message role for role-sensitive renames.
|
|
99
|
+
* @returns a copy whose kind is the producer's own kind and whose `plugin` field is dropped.
|
|
100
|
+
*/
|
|
101
|
+
function rewritePluginSource(source, seq, role) {
|
|
102
|
+
const plugin = source["plugin"];
|
|
103
|
+
if (typeof plugin !== "string") throw new SessionFormatError(`plugin source at seq ${seq} is not canonical: plugin requires a string`);
|
|
104
|
+
const kind = producerKind(plugin, role);
|
|
105
|
+
if (Object.keys(source).length === 2) return { kind };
|
|
106
|
+
return Object.fromEntries(Object.entries(source).filter(([key]) => key !== "plugin").map(([key, item]) => [key, key === "kind" ? kind : item]));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Convert released plugin wrappers while retaining every direct source kind and its metadata.
|
|
110
|
+
* @param source - decoded V3 message source.
|
|
111
|
+
* @param seq - event sequence for diagnostics.
|
|
112
|
+
* @param role - enclosing message role for role-sensitive producer mappings.
|
|
113
|
+
* @returns the converted source with every non-identity field preserved.
|
|
114
|
+
*/
|
|
115
|
+
function rewriteV3MessageSource(source, seq, role) {
|
|
116
|
+
const kind = source["kind"];
|
|
117
|
+
if (typeof kind !== "string" || kind.length === 0) throw new SessionFormatError(`message source at seq ${seq} requires a nonempty kind`);
|
|
118
|
+
if (kind === "plugin") return rewritePluginSource(source, seq, role);
|
|
119
|
+
return source;
|
|
120
|
+
}
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region lib/types/message-sources.js
|
|
123
|
+
/** Native source admission preserves unknown attribution and refuses retired plugin wrappers. */
|
|
124
|
+
function source(message) {
|
|
125
|
+
const value = message["source"];
|
|
126
|
+
if (!isSessionFormatJsonObject(value) || typeof value["kind"] !== "string" || value["kind"].length === 0 || value["kind"] === "plugin") throw new SessionFormatError("format v4 message requires a producer-owned source kind");
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Validate producer attribution in every declared durable message slot.
|
|
130
|
+
* @param event - complete native V4 event before Session adoption.
|
|
131
|
+
*/
|
|
132
|
+
function assertV4MessageSources(event) {
|
|
133
|
+
mapEventMessages(event, (message) => {
|
|
134
|
+
source(message);
|
|
135
|
+
return message;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Refuse retired source syntax even after a malformed recoverable row; leave incomplete messages to decoding.
|
|
140
|
+
* @param row - parsed row whose incomplete values have not been classified by the scanner.
|
|
141
|
+
*/
|
|
142
|
+
function assertV4SourceRowAdmission(row) {
|
|
143
|
+
if (!isSessionFormatJsonObject(row) || !isSessionFormatJsonObject(row["data"])) return;
|
|
144
|
+
const data = row["data"];
|
|
145
|
+
const messages = row["type"] === "user/message" ? [data] : row["type"] === "system/message" || row["type"] === "assistant/message" || row["type"] === "tool/result" ? [data["message"]] : row["type"] === "agent/inbox/spliced" ? data["inserted"] : row["type"] === "session/title-llm-request" ? data["messages"] : [];
|
|
146
|
+
if (!Array.isArray(messages)) return;
|
|
147
|
+
for (const message of messages) {
|
|
148
|
+
if (!isSessionFormatJsonObject(message)) continue;
|
|
149
|
+
const value = message["source"];
|
|
150
|
+
if (isSessionFormatJsonObject(value) && value["kind"] === "plugin") source(message);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region lib/types/retired-syntax.js
|
|
155
|
+
/** Retired native syntax remains a hard refusal even after a recoverable physical-row failure. */
|
|
156
|
+
function assertBlock(block, subject) {
|
|
157
|
+
if (isSessionFormatJsonObject(block) && block["type"] === "tool-result") throw new SessionFormatError(`${subject} must not contain a released tool-result wrapper`);
|
|
158
|
+
}
|
|
159
|
+
function assertContent$1(content, subject) {
|
|
160
|
+
if (Array.isArray(content)) content.forEach((block) => {
|
|
161
|
+
assertBlock(block, subject);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
function assertMessageContent(message, subject) {
|
|
165
|
+
if (isSessionFormatJsonObject(message)) assertContent$1(message["content"], subject);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Refuse retired headers, PTC tags, and tool-result blocks in interpreted content slots.
|
|
169
|
+
* Tool arguments, replay state, schema parameters, and nested extension values remain opaque.
|
|
170
|
+
* @param row - parsed physical row or complete native logical event.
|
|
171
|
+
*/
|
|
172
|
+
function assertV4RetiredSyntax(row) {
|
|
173
|
+
if (!isSessionFormatJsonObject(row)) return;
|
|
174
|
+
if ((row["type"] === "tool/code-dispatch-start" || row["type"] === "tool/code-dispatch") && row["ignorable"] !== true) throw new SessionFormatUnsupportedMigrationError(`format v4 rejects retired event type ${row["type"]}`);
|
|
175
|
+
const data = row["data"];
|
|
176
|
+
if (row["type"] === "request/header") {
|
|
177
|
+
if (!isSessionFormatJsonObject(data) || !isSessionFormatJsonObject(data["header"])) throw new SessionFormatError("format v4 request/header requires data and header objects");
|
|
178
|
+
if (Object.hasOwn(data["header"], "system")) throw new SessionFormatError("format v4 request/header rejects retired header.system");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (!isSessionFormatJsonObject(data)) return;
|
|
182
|
+
const subject = `format v4 ${String(row["type"])} at seq ${String(row["seq"])} content`;
|
|
183
|
+
switch (row["type"]) {
|
|
184
|
+
case "user/message":
|
|
185
|
+
assertContent$1(data["content"], subject);
|
|
186
|
+
break;
|
|
187
|
+
case "developer/message":
|
|
188
|
+
case "assistant/message":
|
|
189
|
+
case "team/message/queued":
|
|
190
|
+
assertMessageContent(data["message"], subject);
|
|
191
|
+
break;
|
|
192
|
+
case "agent/inbox/spliced":
|
|
193
|
+
case "session/title-llm-request": {
|
|
194
|
+
const messages = data[row["type"] === "agent/inbox/spliced" ? "inserted" : "messages"];
|
|
195
|
+
if (Array.isArray(messages)) messages.forEach((message) => {
|
|
196
|
+
assertMessageContent(message, subject);
|
|
197
|
+
});
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
case "compaction/summary":
|
|
201
|
+
assertContent$1(data["summary"], subject);
|
|
202
|
+
assertContent$1(data["rawOutput"], subject);
|
|
203
|
+
break;
|
|
204
|
+
case "tool/ptc-dispatch":
|
|
205
|
+
assertContent$1(data["content"], subject);
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
if ((row["type"] === "assistant/message" || row["type"] === "assistant/attempt") && Array.isArray(data["stream"])) for (const entry of data["stream"]) {
|
|
209
|
+
if (!isSessionFormatJsonObject(entry) || entry["type"] !== "chunk") continue;
|
|
210
|
+
const chunk = entry["chunk"];
|
|
211
|
+
if (!isSessionFormatJsonObject(chunk)) continue;
|
|
212
|
+
if (chunk["type"] === "block-end") assertBlock(chunk["block"], subject);
|
|
213
|
+
if (chunk["type"] === "block-start" && chunk["blockType"] === "tool-result") throw new SessionFormatError(`${subject} must not contain a released tool-result wrapper`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
//#endregion
|
|
217
|
+
//#region lib/types/system-message.js
|
|
218
|
+
/** Native system-message fields checked before physical recovery can discard a row. */
|
|
219
|
+
function record$1(value, subject) {
|
|
220
|
+
if (!isSessionFormatJsonObject(value)) throw new SessionFormatError(`format v4 ${subject} requires an object`);
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
function string(value, subject, nonempty = false) {
|
|
224
|
+
if (typeof value !== "string" || nonempty && value.length === 0) throw new SessionFormatError(`format v4 ${subject} requires a string${nonempty ? " with content" : ""}`);
|
|
225
|
+
}
|
|
226
|
+
function positive(value, subject) {
|
|
227
|
+
if (sessionFormatCount(value, subject) === 0) throw new SessionFormatError(`format v4 ${subject} must be positive`);
|
|
228
|
+
}
|
|
229
|
+
function image(value) {
|
|
230
|
+
const attachment = record$1(value, "system image attachment");
|
|
231
|
+
string(attachment["attachmentId"], "system image attachmentId", true);
|
|
232
|
+
if (![
|
|
233
|
+
"image/png",
|
|
234
|
+
"image/jpeg",
|
|
235
|
+
"image/webp",
|
|
236
|
+
"image/gif"
|
|
237
|
+
].includes(attachment["mediaType"])) throw new SessionFormatError("format v4 system image requires a supported mediaType");
|
|
238
|
+
sessionFormatCount(attachment["bytes"], "system image bytes");
|
|
239
|
+
positive(attachment["width"], "system image width");
|
|
240
|
+
positive(attachment["height"], "system image height");
|
|
241
|
+
if (attachment["name"] !== void 0) string(attachment["name"], "system image name");
|
|
242
|
+
if (attachment["originalDimensions"] !== void 0) {
|
|
243
|
+
const original = record$1(attachment["originalDimensions"], "system image originalDimensions");
|
|
244
|
+
positive(original["width"], "system image original width");
|
|
245
|
+
positive(original["height"], "system image original height");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Validate system fields independently of historical message representations; preserve additional JSON fields.
|
|
250
|
+
* @param row - complete logical event or parsed physical row before corruption recovery.
|
|
251
|
+
*/
|
|
252
|
+
function assertV4SystemMessageFields(row) {
|
|
253
|
+
if (!isSessionFormatJsonObject(row) || row["type"] !== "system/message") return;
|
|
254
|
+
const data = record$1(row["data"], "system/message data");
|
|
255
|
+
positive(data["turn"], "system/message turn");
|
|
256
|
+
positive(data["step"], "system/message step");
|
|
257
|
+
const message = record$1(data["message"], "system message");
|
|
258
|
+
string(message["id"], "system message id", true);
|
|
259
|
+
if (message["role"] !== "system") throw new SessionFormatError("format v4 system message requires system role");
|
|
260
|
+
const content = message["content"];
|
|
261
|
+
if (!Array.isArray(content)) throw new SessionFormatError("format v4 system message requires content array");
|
|
262
|
+
for (const value of content) {
|
|
263
|
+
const block = record$1(value, "system content block");
|
|
264
|
+
string(block["type"], "system content type", true);
|
|
265
|
+
switch (block["type"]) {
|
|
266
|
+
case "text":
|
|
267
|
+
case "reasoning":
|
|
268
|
+
string(block["text"], "system content text");
|
|
269
|
+
break;
|
|
270
|
+
case "tool-call":
|
|
271
|
+
string(block["id"], "system tool-call id", true);
|
|
272
|
+
string(block["name"], "system tool-call name", true);
|
|
273
|
+
string(block["arguments"], "system tool-call arguments");
|
|
274
|
+
break;
|
|
275
|
+
case "image":
|
|
276
|
+
image(block["attachment"]);
|
|
277
|
+
break;
|
|
278
|
+
case "tool-result": throw new SessionFormatError("format v4 system content rejects retired tool-result wrappers");
|
|
279
|
+
default: break;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region lib/types/developer.js
|
|
285
|
+
/** Native V4 developer-message and deferred-tool-schema validation. */
|
|
286
|
+
function assertToolChange(block, developer) {
|
|
287
|
+
if (!isSessionFormatJsonObject(block) || block["type"] !== "tool-addition" && block["type"] !== "tool-removal") return;
|
|
288
|
+
if (!developer) throw new SessionFormatError(`format v4 ${block["type"]} requires developer role`);
|
|
289
|
+
if (typeof block["toolName"] !== "string" || block["toolName"].length === 0) throw new SessionFormatError(`format v4 ${block["type"]} requires a nonempty toolName`);
|
|
290
|
+
if (block["type"] === "tool-addition" && Object.hasOwn(block, "tool")) throw new SessionFormatError("format v4 tool-addition must omit inline tool definitions");
|
|
291
|
+
}
|
|
292
|
+
function assertContent(value, developer = false) {
|
|
293
|
+
if (Array.isArray(value)) value.forEach((block) => {
|
|
294
|
+
assertToolChange(block, developer);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
function assertDeveloperMessage(message) {
|
|
298
|
+
assertContent(message["content"], true);
|
|
299
|
+
const source = message["source"];
|
|
300
|
+
if (typeof message["id"] !== "string" || message["id"].length === 0 || !Array.isArray(message["content"]) || !isSessionFormatJsonObject(source) || typeof source["kind"] !== "string" || source["kind"].length === 0 || source["kind"] === "plugin") throw new SessionFormatError("format v4 developer message requires id, role, content, and a producer-owned source");
|
|
301
|
+
}
|
|
302
|
+
/** Ordinary-message corruption belongs to the decoder; recognized V4-only values are refused here. */
|
|
303
|
+
function assertOrdinaryMessage(value) {
|
|
304
|
+
if (!isSessionFormatJsonObject(value)) return;
|
|
305
|
+
if (value["role"] === "developer") throw new SessionFormatError("format v4 developer/message and developer role must occur together");
|
|
306
|
+
assertContent(value["content"]);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Validate native V4 developer messages, tool-change blocks, and deferred tool schemas.
|
|
310
|
+
* Additional developer JSON fields are preserved. Malformed legacy message
|
|
311
|
+
* slots remain available to recoverable decoding.
|
|
312
|
+
* @param event - logical event or physical row before source-event range decoding.
|
|
313
|
+
*/
|
|
314
|
+
function assertV4DeveloperData(event) {
|
|
315
|
+
const data = event.data;
|
|
316
|
+
if (!isSessionFormatJsonObject(data)) {
|
|
317
|
+
if (event.type === "developer/message") throw new SessionFormatError("format v4 developer/message data must be an object");
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (event.type === "developer/message") {
|
|
321
|
+
const message = data["message"];
|
|
322
|
+
if (!isSessionFormatJsonObject(message) || message["role"] !== "developer") throw new SessionFormatError("format v4 developer/message requires turn, step, and a developer message");
|
|
323
|
+
for (const field of ["turn", "step"]) if (sessionFormatCount(data[field], `developer/message ${field}`) === 0) throw new SessionFormatError(`developer/message ${field} must be positive`);
|
|
324
|
+
assertDeveloperMessage(message);
|
|
325
|
+
if (message["content"].some((block) => isSessionFormatJsonObject(block) && block["type"] === "tool-addition")) sessionFormatCount(data["headerSeq"], "developer/message headerSeq");
|
|
326
|
+
else if (Object.hasOwn(data, "headerSeq")) throw new SessionFormatError("format v4 developer/message must omit headerSeq without tool additions");
|
|
327
|
+
} else switch (event.type) {
|
|
328
|
+
case "user/message":
|
|
329
|
+
assertOrdinaryMessage(data);
|
|
330
|
+
break;
|
|
331
|
+
case "system/message":
|
|
332
|
+
case "assistant/message":
|
|
333
|
+
case "tool/result":
|
|
334
|
+
assertOrdinaryMessage(data["message"]);
|
|
335
|
+
break;
|
|
336
|
+
case "agent/inbox/spliced":
|
|
337
|
+
case "session/title-llm-request": {
|
|
338
|
+
const messages = data[event.type === "agent/inbox/spliced" ? "inserted" : "messages"];
|
|
339
|
+
if (Array.isArray(messages)) messages.forEach(assertOrdinaryMessage);
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (event.type === "compaction/summary") {
|
|
344
|
+
assertContent(data["summary"]);
|
|
345
|
+
assertContent(data["rawOutput"]);
|
|
346
|
+
}
|
|
347
|
+
if ((event.type === "assistant/message" || event.type === "assistant/attempt") && Array.isArray(data["stream"])) for (const entry of data["stream"]) {
|
|
348
|
+
if (!isSessionFormatJsonObject(entry) || entry["type"] !== "chunk") continue;
|
|
349
|
+
const chunk = entry["chunk"];
|
|
350
|
+
if (!isSessionFormatJsonObject(chunk)) continue;
|
|
351
|
+
if (chunk["type"] === "block-end") assertToolChange(chunk["block"], false);
|
|
352
|
+
if (chunk["type"] === "block-start" && (chunk["blockType"] === "tool-addition" || chunk["blockType"] === "tool-removal")) throw new SessionFormatError("format v4 tool-change blocks require developer role");
|
|
353
|
+
}
|
|
354
|
+
if (event.type === "request/header" && isSessionFormatJsonObject(data["header"])) {
|
|
355
|
+
const tools = data["header"]["tools"];
|
|
356
|
+
if (Array.isArray(tools)) for (const tool of tools) {
|
|
357
|
+
if (!isSessionFormatJsonObject(tool) || !Object.hasOwn(tool, "deferLoading")) continue;
|
|
358
|
+
if (tool["deferLoading"] !== true) throw new SessionFormatError("format v4 tool deferLoading must be true when present");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
//#endregion
|
|
363
|
+
//#region lib/types/fork-result.js
|
|
364
|
+
/** Native V4 fork-result identity and not-started error validation. */
|
|
365
|
+
/**
|
|
366
|
+
* Validate fork-generated not-started results without changing their identity or text.
|
|
367
|
+
* Append results carry their own sequence; replacement results retain an earlier identity.
|
|
368
|
+
* @param row - logical event or physical event row.
|
|
369
|
+
*/
|
|
370
|
+
function assertV4ForkResult(row) {
|
|
371
|
+
if (!isSessionFormatJsonObject(row) || row["type"] !== "tool/result") return;
|
|
372
|
+
const data = row["data"];
|
|
373
|
+
if (!isSessionFormatJsonObject(data)) return;
|
|
374
|
+
const error = data["error"];
|
|
375
|
+
const message = data["message"];
|
|
376
|
+
if (!isSessionFormatJsonObject(error) || error["code"] !== "TOOL_NOT_STARTED" || !isSessionFormatJsonObject(message) || typeof message["id"] !== "string" || !message["id"].startsWith("forked-tool-result-")) return;
|
|
377
|
+
const source = message["source"];
|
|
378
|
+
const callId = isSessionFormatJsonObject(source) ? source["callId"] : void 0;
|
|
379
|
+
const prefix = `forked-tool-result-${String(callId)}-`;
|
|
380
|
+
const suffix = message["id"].slice(prefix.length);
|
|
381
|
+
const operation = row["surfaceOp"];
|
|
382
|
+
const replacement = isSessionFormatJsonObject(operation) && operation["op"] === "replace";
|
|
383
|
+
const sequence = Number(suffix);
|
|
384
|
+
const content = message["content"];
|
|
385
|
+
const text = Array.isArray(content) && content.length === 1 ? content[0] : void 0;
|
|
386
|
+
const sourceEventSeqs = row["sourceEventSeqs"];
|
|
387
|
+
if (typeof callId !== "string" || !message["id"].startsWith(prefix) || !/^(0|[1-9]\d*)$/.test(suffix) || !Number.isSafeInteger(sequence) || (replacement ? typeof row["seq"] !== "number" || sequence >= row["seq"] : sequence !== row["seq"]) || error["name"] !== "ToolNotStartedError" || !replacement && (sourceEventSeqs !== void 0 || operation !== "append") || replacement && (!Array.isArray(sourceEventSeqs) || sourceEventSeqs.length !== 1 || sourceEventSeqs[0] !== sequence) || message["role"] !== "tool" || message["isError"] !== true || message["toolCallId"] !== callId || !isSessionFormatJsonObject(source) || source["kind"] !== "tool" || !isSessionFormatJsonObject(text) || text["type"] !== "text" || typeof text["text"] !== "string") throw new SessionFormatError("invalid V4 not-started fork result");
|
|
388
|
+
}
|
|
389
|
+
//#endregion
|
|
390
|
+
//#region lib/types/tool-role.js
|
|
391
|
+
/** First-class tool-role messages in the V4 representation. */
|
|
392
|
+
const WRAPPER_FIELDS = new Set([
|
|
393
|
+
"type",
|
|
394
|
+
"toolCallId",
|
|
395
|
+
"content",
|
|
396
|
+
"isError"
|
|
397
|
+
]);
|
|
398
|
+
const MESSAGE_FIELDS = new Set([
|
|
399
|
+
"id",
|
|
400
|
+
"role",
|
|
401
|
+
"source",
|
|
402
|
+
"content"
|
|
403
|
+
]);
|
|
404
|
+
/** Preserve unknown fields without merging their original message and result owners. */
|
|
405
|
+
function extensionFields(value, fields, owner) {
|
|
406
|
+
return Object.fromEntries(Object.entries(value).filter(([key]) => !fields.has(key)).map(([key, field]) => [`plugin:${owner}:${key}`, field]));
|
|
407
|
+
}
|
|
408
|
+
function resultContent(value, subject) {
|
|
409
|
+
if (!Array.isArray(value)) throw new SessionFormatError(`${subject} tool-result content must be an array`);
|
|
410
|
+
if (value.some((block) => isSessionFormatJsonObject(block) && block["type"] === "tool-result")) throw new SessionFormatUnsupportedMigrationError(`${subject} contains a nested tool-result unsupported by this converter`);
|
|
411
|
+
return value;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Lift one released-V3 wrapper tool/result row into the first-class V4 message.
|
|
415
|
+
* Validate the wrapper before dropping it so malformed historical content is
|
|
416
|
+
* rejected instead of being silently truncated.
|
|
417
|
+
* @param event - released wrapper tool/result event.
|
|
418
|
+
* @returns the same event with a first-class tool-role message.
|
|
419
|
+
* @throws {SessionFormatError} when the wrapper, tool source, content, or error flag is malformed.
|
|
420
|
+
* @throws {SessionFormatUnsupportedMigrationError} when the result contains another result.
|
|
421
|
+
*/
|
|
422
|
+
function liftToolResult(event) {
|
|
423
|
+
if (event.type !== "tool/result" || !isSessionFormatJsonObject(event.data)) return event;
|
|
424
|
+
const data = event.data;
|
|
425
|
+
const message = data["message"];
|
|
426
|
+
if (!isSessionFormatJsonObject(message) || message["role"] !== "user") return event;
|
|
427
|
+
const source = message["source"];
|
|
428
|
+
const callId = isSessionFormatJsonObject(source) ? source["callId"] : void 0;
|
|
429
|
+
const content = message["content"];
|
|
430
|
+
const block = Array.isArray(content) && content.length === 1 ? content[0] : void 0;
|
|
431
|
+
const wrapper = isSessionFormatJsonObject(block) ? block : void 0;
|
|
432
|
+
const id = message["id"];
|
|
433
|
+
if (typeof id !== "string" || id.length === 0 || !isSessionFormatJsonObject(source) || source["kind"] !== "tool" || typeof callId !== "string" || callId.length === 0 || wrapper === void 0 || wrapper["type"] !== "tool-result" || wrapper["toolCallId"] !== callId) throw new SessionFormatError(`format v3 ${event.type} at seq ${event.seq} requires exactly one tool-result wrapper matching its tool source`);
|
|
434
|
+
const isError = wrapper["isError"];
|
|
435
|
+
if (isError !== void 0 && typeof isError !== "boolean") throw new SessionFormatError(`format v3 ${event.type} at seq ${event.seq} tool-result isError must be boolean`);
|
|
436
|
+
const targetMessage = {
|
|
437
|
+
role: "tool",
|
|
438
|
+
source,
|
|
439
|
+
toolCallId: callId,
|
|
440
|
+
content: resultContent(wrapper["content"], `format v3 ${event.type} at seq ${event.seq}`),
|
|
441
|
+
...isError === void 0 ? {} : { isError },
|
|
442
|
+
id,
|
|
443
|
+
...extensionFields(message, MESSAGE_FIELDS, "message"),
|
|
444
|
+
...extensionFields(wrapper, WRAPPER_FIELDS, "result")
|
|
445
|
+
};
|
|
446
|
+
return {
|
|
447
|
+
...event,
|
|
448
|
+
data: {
|
|
449
|
+
...data,
|
|
450
|
+
message: targetMessage
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Validate the native V4 first-class tool-role message of one tool/result row.
|
|
456
|
+
* @param event - canonical V4 event.
|
|
457
|
+
* @throws {SessionFormatError} when the row is not an exact first-class tool result.
|
|
458
|
+
*/
|
|
459
|
+
function assertV4ToolResultMessage(event) {
|
|
460
|
+
if (event.type !== "tool/result") return;
|
|
461
|
+
const subject = `format v4 ${event.type} at seq ${event.seq}`;
|
|
462
|
+
const data = event.data;
|
|
463
|
+
if (!isSessionFormatJsonObject(data)) throw new SessionFormatError(`${subject} data must be an object`);
|
|
464
|
+
const message = data["message"];
|
|
465
|
+
if (!isSessionFormatJsonObject(message)) throw new SessionFormatError(`${subject} message must be an object`);
|
|
466
|
+
const id = message["id"];
|
|
467
|
+
const role = message["role"];
|
|
468
|
+
const toolCallId = message["toolCallId"];
|
|
469
|
+
const source = message["source"];
|
|
470
|
+
const isError = message["isError"];
|
|
471
|
+
const content = message["content"];
|
|
472
|
+
const sourceCallId = isSessionFormatJsonObject(source) ? source["callId"] : void 0;
|
|
473
|
+
if (typeof id !== "string" || id.length === 0) throw new SessionFormatError(`${subject} requires a first-class message with a string id`);
|
|
474
|
+
if (role !== "tool") throw new SessionFormatError(`${subject} requires a tool-role message`);
|
|
475
|
+
if (typeof toolCallId !== "string" || toolCallId.length === 0 || sourceCallId !== toolCallId) throw new SessionFormatError(`${subject} requires toolCallId matching its tool source`);
|
|
476
|
+
if (!isSessionFormatJsonObject(source) || source["kind"] !== "tool") throw new SessionFormatError(`${subject} requires a tool source`);
|
|
477
|
+
if (!Array.isArray(content)) throw new SessionFormatError(`${subject} requires array content`);
|
|
478
|
+
if (content.some((block) => isSessionFormatJsonObject(block) && block["type"] === "tool-result")) throw new SessionFormatError(`${subject} content must not contain a released tool-result wrapper`);
|
|
479
|
+
if (isError !== void 0 && typeof isError !== "boolean") throw new SessionFormatError(`${subject} isError must be boolean when present`);
|
|
480
|
+
if (data["error"] !== void 0 && isError !== true) throw new SessionFormatError(`${subject} carries error metadata for a non-error tool result`);
|
|
481
|
+
}
|
|
482
|
+
//#endregion
|
|
483
|
+
//#region lib/types/relationships.js
|
|
484
|
+
/** Mandatory native V4 relationships; incomplete tails retain their open transactions. */
|
|
485
|
+
const SURFACE_TYPES = new Set([
|
|
486
|
+
"system/message",
|
|
487
|
+
"user/message",
|
|
488
|
+
"developer/message",
|
|
489
|
+
"assistant/message",
|
|
490
|
+
"tool/result"
|
|
491
|
+
]);
|
|
492
|
+
const STEP_EVENT_TYPES = new Set([
|
|
493
|
+
"system/message",
|
|
494
|
+
"developer/message",
|
|
495
|
+
"assistant/attempt"
|
|
496
|
+
]);
|
|
497
|
+
const RELATIONSHIP_TYPES = new Set([
|
|
498
|
+
...SURFACE_TYPES,
|
|
499
|
+
...STEP_EVENT_TYPES,
|
|
500
|
+
"turn/start",
|
|
501
|
+
"turn/end",
|
|
502
|
+
"step/start",
|
|
503
|
+
"step/end",
|
|
504
|
+
"tool/call",
|
|
505
|
+
"request/header",
|
|
506
|
+
"request/context",
|
|
507
|
+
"tool/ptc-dispatch-start",
|
|
508
|
+
"tool/ptc-dispatch",
|
|
509
|
+
"llm/retry",
|
|
510
|
+
"llm/retry-started",
|
|
511
|
+
"session/title",
|
|
512
|
+
"session/title-llm-request",
|
|
513
|
+
"command/run",
|
|
514
|
+
"command/done",
|
|
515
|
+
"compaction/start",
|
|
516
|
+
"compaction/summary",
|
|
517
|
+
"compaction/end",
|
|
518
|
+
"compaction/prune",
|
|
519
|
+
"session/end-seed"
|
|
520
|
+
]);
|
|
521
|
+
function record(value, subject) {
|
|
522
|
+
if (!isSessionFormatJsonObject(value)) throw new SessionFormatError(`${subject} requires an object`);
|
|
523
|
+
return value;
|
|
524
|
+
}
|
|
525
|
+
function text(value, subject) {
|
|
526
|
+
if (typeof value !== "string" || value.length === 0) throw new SessionFormatError(`${subject} requires a nonempty string`);
|
|
527
|
+
return value;
|
|
528
|
+
}
|
|
529
|
+
function array(value, subject) {
|
|
530
|
+
if (!Array.isArray(value)) throw new SessionFormatError(`${subject} requires an array`);
|
|
531
|
+
return value;
|
|
532
|
+
}
|
|
533
|
+
function earlier(value, seq, subject) {
|
|
534
|
+
const coordinate = sessionFormatCount(value, subject);
|
|
535
|
+
if (coordinate >= seq) throw new SessionFormatError(`${subject} must name an earlier event`);
|
|
536
|
+
return coordinate;
|
|
537
|
+
}
|
|
538
|
+
/** One native artifact's lifecycle state; no state survives between reads. */
|
|
539
|
+
var Relationships = class {
|
|
540
|
+
artifact;
|
|
541
|
+
knownEventTypes;
|
|
542
|
+
turn = null;
|
|
543
|
+
step = null;
|
|
544
|
+
nextTurn = 1;
|
|
545
|
+
nextStep = 1;
|
|
546
|
+
provider;
|
|
547
|
+
surface = [];
|
|
548
|
+
protectedHead;
|
|
549
|
+
compaction;
|
|
550
|
+
tools = /* @__PURE__ */ new Map();
|
|
551
|
+
dispatches = /* @__PURE__ */ new Map();
|
|
552
|
+
retries = [];
|
|
553
|
+
startedRetries = /* @__PURE__ */ new Set();
|
|
554
|
+
commands = /* @__PURE__ */ new Set();
|
|
555
|
+
orphanCompactions = /* @__PURE__ */ new Set();
|
|
556
|
+
constructor(artifact, knownEventTypes) {
|
|
557
|
+
this.artifact = artifact;
|
|
558
|
+
this.knownEventTypes = knownEventTypes;
|
|
559
|
+
let start;
|
|
560
|
+
for (const event of artifact.events) {
|
|
561
|
+
if (!knownEventTypes.has(event.type)) continue;
|
|
562
|
+
if (event.type === "compaction/start") start = event.seq;
|
|
563
|
+
if (event.type === "compaction/end") start = void 0;
|
|
564
|
+
if (event.type === "session/end-seed") {
|
|
565
|
+
if (start !== void 0) this.orphanCompactions.add(start);
|
|
566
|
+
start = void 0;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
requireTurn(type) {
|
|
571
|
+
if (this.turn === null) throw new SessionFormatError(`${type} is outside an open turn`);
|
|
572
|
+
}
|
|
573
|
+
requireStep(event, data) {
|
|
574
|
+
if (this.turn === null || this.step === null || data["turn"] !== this.turn || data["step"] !== this.step) throw new SessionFormatError(`${event.type} does not match an open turn and step`);
|
|
575
|
+
}
|
|
576
|
+
closeTools(type) {
|
|
577
|
+
if (this.tools.size !== 0) throw new SessionFormatError(`${type} leaves unresolved tool call ${String(this.tools.keys().next().value)}`);
|
|
578
|
+
this.tools.clear();
|
|
579
|
+
}
|
|
580
|
+
developer(event, data) {
|
|
581
|
+
if (data["headerSeq"] === void 0) return;
|
|
582
|
+
const headerSeq = earlier(data["headerSeq"], event.seq, "developer/message headerSeq");
|
|
583
|
+
const headerEvent = this.artifact.events[headerSeq];
|
|
584
|
+
if (headerEvent?.type !== "request/header") throw new SessionFormatError("developer/message headerSeq must reference an earlier request/header");
|
|
585
|
+
if (!this.knownEventTypes.has(headerEvent.type)) throw new SessionFormatError("developer/message headerSeq must reference a known request/header");
|
|
586
|
+
const tools = record(record(headerEvent.data, "request/header data")["header"], "request header")["tools"];
|
|
587
|
+
const content = array(record(data["message"], "developer message")["content"], "developer content");
|
|
588
|
+
for (const value of content) {
|
|
589
|
+
if (!isSessionFormatJsonObject(value) || value["type"] !== "tool-addition") continue;
|
|
590
|
+
const toolName = value["toolName"];
|
|
591
|
+
const definitions = Array.isArray(tools) ? tools.filter((tool) => isSessionFormatJsonObject(tool) && tool["name"] === toolName) : [];
|
|
592
|
+
if (definitions.length !== 1) throw new SessionFormatError(`developer/message tool-addition "${toolName}" must name exactly one tool in headerSeq ${headerSeq}`);
|
|
593
|
+
const definition = definitions[0];
|
|
594
|
+
if (typeof definition["description"] !== "string" || !isSessionFormatJsonObject(definition["parameters"])) throw new SessionFormatError(`developer/message tool-addition "${toolName}" requires a complete tool definition in headerSeq ${headerSeq}`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
foldSurface(event) {
|
|
598
|
+
if (!SURFACE_TYPES.has(event.type)) return;
|
|
599
|
+
if (event.type === "system/message" && this.surface.length > 0 && this.protectedHead === void 0) throw new SessionFormatError("system/message requires a protected first surface head");
|
|
600
|
+
if (event["surfaceOp"] === "append") {
|
|
601
|
+
if (event.type === "system/message" && this.surface.length === 0) this.protectedHead = event.seq;
|
|
602
|
+
this.surface.push(event.seq);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const operation = record(event["surfaceOp"], `${event.type} surfaceOp`);
|
|
606
|
+
const first = this.surface.indexOf(earlier(operation["startSeq"], event.seq, "replacement start"));
|
|
607
|
+
const last = this.surface.indexOf(earlier(operation["endSeq"], event.seq, "replacement end"));
|
|
608
|
+
if (first < 0 || last < first) throw new SessionFormatError(`${event.type} replacement range is not on the current surface`);
|
|
609
|
+
const removed = this.surface.slice(first, last + 1);
|
|
610
|
+
const sources = array(event["sourceEventSeqs"], "replacement sourceEventSeqs");
|
|
611
|
+
if (removed.some((seq) => !sources.includes(seq))) throw new SessionFormatError("replacement sourceEventSeqs omit a shadowed surface node");
|
|
612
|
+
if (this.protectedHead !== void 0 && removed.includes(this.protectedHead)) {
|
|
613
|
+
if (event.type !== "system/message" || removed.length !== 1) throw new SessionFormatError("surface replacement cannot shadow the protected system head");
|
|
614
|
+
this.protectedHead = event.seq;
|
|
615
|
+
}
|
|
616
|
+
this.surface.splice(first, removed.length, event.seq);
|
|
617
|
+
}
|
|
618
|
+
tool(event, data) {
|
|
619
|
+
if (event.type === "tool/result" && event["surfaceOp"] !== "append") {
|
|
620
|
+
this.requireTurn(event.type);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
this.requireStep(event, data);
|
|
624
|
+
if (event.type === "assistant/message") {
|
|
625
|
+
const message = record(data["message"], "assistant message");
|
|
626
|
+
for (const value of array(message["content"], "assistant content")) {
|
|
627
|
+
const block = record(value, "assistant content block");
|
|
628
|
+
if (block["type"] !== "tool-call") continue;
|
|
629
|
+
const id = text(block["id"], "tool call id");
|
|
630
|
+
if (this.tools.has(id)) throw new SessionFormatError(`assistant/message repeats advertised tool call ${id}`);
|
|
631
|
+
this.tools.set(id, {
|
|
632
|
+
name: block["name"],
|
|
633
|
+
arguments: block["arguments"],
|
|
634
|
+
started: false
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
const message = event.type === "tool/result" ? record(data["message"], "tool result message") : void 0;
|
|
640
|
+
const id = text(message === void 0 ? data["callId"] : message["toolCallId"], "tool call id");
|
|
641
|
+
const pending = this.tools.get(id);
|
|
642
|
+
if (pending === void 0) throw new SessionFormatError(`${event.type} ${id} has no advertised tool lifecycle`);
|
|
643
|
+
if (message === void 0) {
|
|
644
|
+
if (pending.started || pending.name !== data["name"] || pending.arguments !== data["arguments"]) throw new SessionFormatError(`tool/call ${id} does not match one advertised tool call`);
|
|
645
|
+
pending.started = true;
|
|
646
|
+
} else {
|
|
647
|
+
if (!pending.started && !notStartedRepair(event, data, message, id)) throw new SessionFormatError(`tool/result ${id} is not the exact TOOL_NOT_STARTED repair`);
|
|
648
|
+
this.tools.delete(id);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
dispatch(event, data) {
|
|
652
|
+
this.requireTurn(event.type);
|
|
653
|
+
const id = text(data["subCallId"], "PTC subCallId");
|
|
654
|
+
const root = text(data["rootCallId"], "PTC rootCallId");
|
|
655
|
+
const parent = text(data["parentCallId"], "PTC parentCallId");
|
|
656
|
+
const existing = this.dispatches.get(id);
|
|
657
|
+
if (existing !== void 0 && existing.data["rootCallId"] !== root) throw new SessionFormatError("PTC dispatch changes its rootCallId");
|
|
658
|
+
if (parent !== root && this.dispatches.get(parent)?.data["rootCallId"] !== root) throw new SessionFormatError("PTC parentCallId does not belong to rootCallId");
|
|
659
|
+
if (event.type === "tool/ptc-dispatch-start") {
|
|
660
|
+
if (existing !== void 0) throw new SessionFormatError("PTC dispatch repeats subCallId");
|
|
661
|
+
this.dispatches.set(id, {
|
|
662
|
+
data,
|
|
663
|
+
settled: false
|
|
664
|
+
});
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (existing === void 0 || existing.settled) throw new SessionFormatError("PTC dispatch has no unique start");
|
|
668
|
+
if ([
|
|
669
|
+
"rootCallId",
|
|
670
|
+
"parentCallId",
|
|
671
|
+
"name",
|
|
672
|
+
"arguments"
|
|
673
|
+
].some((key) => !isDeepStrictEqual(existing.data[key], data[key]))) throw new SessionFormatError("PTC dispatch does not match its start");
|
|
674
|
+
existing.settled = true;
|
|
675
|
+
}
|
|
676
|
+
retry(event, data) {
|
|
677
|
+
const id = text(data["retryId"], "retryId");
|
|
678
|
+
const attempt = sessionFormatCount(data["retry"], "retry");
|
|
679
|
+
if (event.type === "llm/retry-started") {
|
|
680
|
+
const scheduled = this.retries.find((item) => item["retryId"] === id && item["retry"] === attempt);
|
|
681
|
+
if (scheduled === void 0) throw new SessionFormatError("llm/retry-started pairs no prior scheduled attempt");
|
|
682
|
+
if (scheduled["turn"] !== data["turn"] || scheduled["step"] !== data["step"]) throw new SessionFormatError("llm/retry-started changes scheduled coordinates");
|
|
683
|
+
const key = JSON.stringify([id, attempt]);
|
|
684
|
+
if (this.startedRetries.has(key)) throw new SessionFormatError("llm/retry-started repeats one scheduled attempt");
|
|
685
|
+
this.startedRetries.add(key);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (this.turn === null || data["turn"] !== this.turn || data["step"] !== (this.step ?? this.nextStep - 1)) throw new SessionFormatError("llm/retry does not match the current turn and step");
|
|
689
|
+
if (data["provider"] !== this.provider) throw new SessionFormatError("llm/retry provider does not match the open request/header");
|
|
690
|
+
const prior = this.retries.findLast((item) => [
|
|
691
|
+
"turn",
|
|
692
|
+
"step",
|
|
693
|
+
"provider",
|
|
694
|
+
"policyKey"
|
|
695
|
+
].every((key) => item[key] === data[key]));
|
|
696
|
+
if (attempt !== (prior === void 0 ? 1 : sessionFormatCount(prior["retry"], "prior retry") + 1)) throw new SessionFormatError("llm/retry skips its policy attempt sequence");
|
|
697
|
+
if (prior === void 0 ? this.retries.some((item) => item["retryId"] === id) : prior["retryId"] !== id) throw new SessionFormatError("llm/retry must keep one retryId per policy chain");
|
|
698
|
+
this.retries.push(data);
|
|
699
|
+
}
|
|
700
|
+
compact(event, data) {
|
|
701
|
+
if (event.type === "compaction/prune" || event.type === "compaction/summary") this.span(event, data);
|
|
702
|
+
if (event.type === "compaction/prune") return;
|
|
703
|
+
if (event.type === "compaction/start") {
|
|
704
|
+
if (this.compaction !== void 0) throw new SessionFormatError("compaction/start overlaps an open compaction");
|
|
705
|
+
const owner = data["turn"] === null ? null : sessionFormatCount(data["turn"], "compaction turn");
|
|
706
|
+
if (owner !== this.turn) throw new SessionFormatError("compaction/start does not match the open turn");
|
|
707
|
+
this.compaction = {
|
|
708
|
+
id: text(data["compactionId"], "compactionId"),
|
|
709
|
+
command: data["sourceCommandId"],
|
|
710
|
+
turn: owner,
|
|
711
|
+
seq: event.seq,
|
|
712
|
+
summarized: false
|
|
713
|
+
};
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
const current = this.compactionOwner(data, event.type);
|
|
717
|
+
if (current.turn !== this.turn) throw new SessionFormatError(`${event.type} does not match the open turn`);
|
|
718
|
+
if (event.type === "compaction/summary") {
|
|
719
|
+
if (current.summarized) throw new SessionFormatError("compaction/summary repeats");
|
|
720
|
+
current.summarized = true;
|
|
721
|
+
} else {
|
|
722
|
+
if (data["turn"] !== current.turn) throw new SessionFormatError("compaction/end changes its owner turn");
|
|
723
|
+
if (data["error"] === void 0 && !current.summarized) throw new SessionFormatError("successful compaction/end requires one summary");
|
|
724
|
+
this.compaction = void 0;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
compactionOwner(data, subject) {
|
|
728
|
+
if (this.compaction === void 0 || this.compaction.id !== data["compactionId"] || this.compaction.command !== data["sourceCommandId"]) throw new SessionFormatError(`${subject} has no matching compaction/start`);
|
|
729
|
+
return this.compaction;
|
|
730
|
+
}
|
|
731
|
+
span(event, data) {
|
|
732
|
+
const range = record(data["shadowedRange"], "compaction shadowedRange");
|
|
733
|
+
const start = this.surface.indexOf(earlier(range["start"], event.seq, "compaction range start"));
|
|
734
|
+
const end = this.surface.indexOf(earlier(range["end"], event.seq, "compaction range end"));
|
|
735
|
+
const seqs = array(data["shadowedSeqs"], "compaction shadowedSeqs");
|
|
736
|
+
if (start < 0 || end < start || !isDeepStrictEqual(this.surface.slice(start, end + 1), seqs)) throw new SessionFormatError(`${event.type} shadowedSeqs do not name an exact current surface span`);
|
|
737
|
+
if (this.protectedHead !== void 0 && seqs.includes(this.protectedHead)) throw new SessionFormatError("compaction cannot shadow the protected system head");
|
|
738
|
+
}
|
|
739
|
+
accept(event) {
|
|
740
|
+
if (!this.knownEventTypes.has(event.type) || !RELATIONSHIP_TYPES.has(event.type)) return;
|
|
741
|
+
const data = record(event.data, event.type);
|
|
742
|
+
if (STEP_EVENT_TYPES.has(event.type)) this.requireStep(event, data);
|
|
743
|
+
if (event.type.startsWith("turn/") && this.compaction !== void 0 && !this.orphanCompactions.has(this.compaction.seq)) throw new SessionFormatError(`${event.type} crosses an open compaction`);
|
|
744
|
+
this.foldSurface(event);
|
|
745
|
+
switch (event.type) {
|
|
746
|
+
case "turn/start":
|
|
747
|
+
if (this.turn !== null || data["turn"] !== this.nextTurn) throw new SessionFormatError("turn/start does not open the expected turn");
|
|
748
|
+
this.turn = this.nextTurn;
|
|
749
|
+
this.nextStep = 1;
|
|
750
|
+
this.tools.clear();
|
|
751
|
+
break;
|
|
752
|
+
case "turn/end":
|
|
753
|
+
if (this.turn === null || data["turn"] !== this.turn || this.step !== null) throw new SessionFormatError("turn/end does not match the open turn with no open step");
|
|
754
|
+
this.closeTools(event.type);
|
|
755
|
+
this.turn = null;
|
|
756
|
+
this.nextTurn += 1;
|
|
757
|
+
break;
|
|
758
|
+
case "step/start":
|
|
759
|
+
if (this.turn === null || data["turn"] !== this.turn || this.step !== null || data["step"] !== this.nextStep) throw new SessionFormatError("step/start does not match the open turn and next step");
|
|
760
|
+
this.step = this.nextStep;
|
|
761
|
+
break;
|
|
762
|
+
case "step/end":
|
|
763
|
+
this.requireStep(event, data);
|
|
764
|
+
this.closeTools(event.type);
|
|
765
|
+
this.step = null;
|
|
766
|
+
this.nextStep += 1;
|
|
767
|
+
break;
|
|
768
|
+
case "assistant/message":
|
|
769
|
+
case "tool/call":
|
|
770
|
+
case "tool/result":
|
|
771
|
+
this.tool(event, data);
|
|
772
|
+
break;
|
|
773
|
+
case "developer/message":
|
|
774
|
+
this.developer(event, data);
|
|
775
|
+
break;
|
|
776
|
+
case "request/header":
|
|
777
|
+
this.requireTurn(event.type);
|
|
778
|
+
this.provider = record(record(data["header"], "request header")["config"], "request config")["provider"];
|
|
779
|
+
break;
|
|
780
|
+
case "request/context":
|
|
781
|
+
this.requireTurn(event.type);
|
|
782
|
+
break;
|
|
783
|
+
case "tool/ptc-dispatch-start":
|
|
784
|
+
case "tool/ptc-dispatch":
|
|
785
|
+
this.dispatch(event, data);
|
|
786
|
+
break;
|
|
787
|
+
case "llm/retry":
|
|
788
|
+
case "llm/retry-started":
|
|
789
|
+
this.retry(event, data);
|
|
790
|
+
break;
|
|
791
|
+
case "session/title":
|
|
792
|
+
case "session/title-llm-request":
|
|
793
|
+
titleSources(this.artifact.events, event, data, this.knownEventTypes);
|
|
794
|
+
break;
|
|
795
|
+
case "command/run": {
|
|
796
|
+
const id = text(data["commandId"], "commandId");
|
|
797
|
+
if (this.commands.has(id)) throw new SessionFormatError(`command/run repeats commandId ${id}`);
|
|
798
|
+
this.commands.add(id);
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
case "command/done":
|
|
802
|
+
if (!this.commands.has(text(data["commandId"], "commandId"))) throw new SessionFormatError("command/done has no prior command/run");
|
|
803
|
+
if (data["sourceEventSeq"] !== void 0) {
|
|
804
|
+
const source = this.artifact.events[earlier(data["sourceEventSeq"], event.seq, "command sourceEventSeq")];
|
|
805
|
+
if (data["kind"] !== "success" || source?.type === "command/run" || source?.type === "command/done") throw new SessionFormatError("command/done has invalid sourceEventSeq");
|
|
806
|
+
}
|
|
807
|
+
break;
|
|
808
|
+
case "compaction/start":
|
|
809
|
+
case "compaction/summary":
|
|
810
|
+
case "compaction/end":
|
|
811
|
+
case "compaction/prune":
|
|
812
|
+
this.compact(event, data);
|
|
813
|
+
break;
|
|
814
|
+
case "user/message": {
|
|
815
|
+
const source = record(data["source"], "user message source");
|
|
816
|
+
if (event["surfaceOp"] !== "append" && source["kind"] === "compact-checkpoint") this.compactionOwner(source, "compaction checkpoint");
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
case "session/end-seed":
|
|
820
|
+
this.compaction = void 0;
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
function notStartedRepair(event, data, message, callId) {
|
|
826
|
+
const error = record(data["error"], "not-started error");
|
|
827
|
+
if (error["name"] !== "ToolNotStartedError" || error["code"] !== "TOOL_NOT_STARTED" || message["isError"] !== true || event["sourceEventSeqs"] !== void 0) return false;
|
|
828
|
+
const id = text(message["id"], "not-started message id");
|
|
829
|
+
if (id.startsWith(`forked-tool-result-${callId}-`)) return true;
|
|
830
|
+
const prefix = `interrupted-tool-result-${callId}-`;
|
|
831
|
+
const suffix = id.slice(prefix.length);
|
|
832
|
+
const content = array(message["content"], "not-started content");
|
|
833
|
+
const block = content[0];
|
|
834
|
+
return id.startsWith(prefix) && /^(0|[1-9]\d*)$/.test(suffix) && Number.isSafeInteger(Number(suffix)) && content.length === 1 && isSessionFormatJsonObject(block) && block["type"] === "text" && block["text"] === "The tool call was interrupted before the Harness recorded it as started. Retry it if it is still needed.";
|
|
835
|
+
}
|
|
836
|
+
function titleSources(events, event, data, knownEventTypes) {
|
|
837
|
+
const references = array(data["messageSeqs"], "title messageSeqs");
|
|
838
|
+
if (event.type === "session/title" && references.length === 0 !== (record(data["source"], "title source")["kind"] === "user")) throw new SessionFormatError("session/title messageSeqs must be empty exactly for a user title");
|
|
839
|
+
const seen = /* @__PURE__ */ new Set();
|
|
840
|
+
for (const value of references) {
|
|
841
|
+
const seq = earlier(value, event.seq, "title messageSeqs");
|
|
842
|
+
const source = events[seq];
|
|
843
|
+
if (seen.has(seq) || source?.type !== "user/message" || !knownEventTypes.has(source.type) || record(record(source.data, "title input")["source"], "title input source")["kind"] !== "user") throw new SessionFormatError(`${event.type} messageSeqs must cite distinct earlier human user/message events`);
|
|
844
|
+
seen.add(seq);
|
|
845
|
+
}
|
|
846
|
+
if (event.type !== "session/title-llm-request") return;
|
|
847
|
+
const messages = array(data["messages"], "title messages");
|
|
848
|
+
const message = record(messages[0], "title message");
|
|
849
|
+
const content = array(message["content"], "title content");
|
|
850
|
+
const block = content[0];
|
|
851
|
+
if (references.length === 0 || messages.length !== 1 || message["role"] !== "user" || record(message["source"], "title source")["kind"] !== "dsh-session-title-llm" || content.length !== 1 || !isSessionFormatJsonObject(block) || block["type"] !== "text") throw new SessionFormatError("session/title-llm-request messages do not represent messageSeqs");
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Validate native V4 lifecycle and ownership facts without rewriting any event.
|
|
855
|
+
* @param artifact - artifact whose V4 envelopes and messages have been admitted.
|
|
856
|
+
* @param knownEventTypes - installed event types whose payloads this reader interprets.
|
|
857
|
+
*/
|
|
858
|
+
function assertV4LifecycleRelationships(artifact, knownEventTypes) {
|
|
859
|
+
const state = new Relationships(artifact, knownEventTypes);
|
|
860
|
+
for (const event of artifact.events) state.accept(event);
|
|
861
|
+
}
|
|
862
|
+
//#endregion
|
|
863
|
+
//#region lib/types/facts.js
|
|
864
|
+
/** Historical child identity reduced to the fields required by a parent's catalog. */
|
|
865
|
+
/**
|
|
866
|
+
* Collect a child's own descriptor without requiring one before its parent catalog is read.
|
|
867
|
+
* @param artifact - validated historical child artifact with its exact inherited cut.
|
|
868
|
+
* @returns compact child identity and descriptor evidence with validated discovery fields for catalog completion.
|
|
869
|
+
* @throws SessionFormatError when a supported own descriptor has invalid discovery fields.
|
|
870
|
+
*/
|
|
871
|
+
function historicalChildCatalogSource(artifact) {
|
|
872
|
+
const header = artifact.header;
|
|
873
|
+
if (header.origin !== "subagent" || header.parentSession === void 0) throw new SessionFormatUnsupportedMigrationError("catalog migration requires a subagent child with a direct parent");
|
|
874
|
+
const descriptors = artifact.events.filter((event) => event.type === "subagent/descriptor" && event.seq >= artifact.inheritedEventCount);
|
|
875
|
+
const source = {
|
|
876
|
+
childId: header.id,
|
|
877
|
+
childCreatedAt: header.createdAt,
|
|
878
|
+
descriptorCount: descriptors.length,
|
|
879
|
+
descriptor: descriptors[0]?.data ?? null
|
|
880
|
+
};
|
|
881
|
+
childCatalogFact(source);
|
|
882
|
+
return source;
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Validate supplemental child evidence supplied across the migration JSON interface.
|
|
886
|
+
* @param value - collected child identity and optional descriptor.
|
|
887
|
+
* @returns validated evidence; descriptor interpretation is deferred until parent membership is known.
|
|
888
|
+
*/
|
|
889
|
+
function childCatalogSource(value) {
|
|
890
|
+
if (!isSessionFormatJsonObject(value) || typeof value["childId"] !== "string" || !Object.hasOwn(value, "descriptor")) throw new SessionFormatUnsupportedMigrationError("catalog migration requires historical child identity and descriptor evidence");
|
|
891
|
+
sessionFormatCount(value["childCreatedAt"], "catalog child creation time");
|
|
892
|
+
sessionFormatCount(value["descriptorCount"], "child descriptor count");
|
|
893
|
+
return value;
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Interpret only the discovery fields of known historical descriptors.
|
|
897
|
+
* @param source - validated child evidence.
|
|
898
|
+
* @returns a catalog fact, or undefined without exactly one supported own descriptor.
|
|
899
|
+
*/
|
|
900
|
+
function childCatalogFact(source) {
|
|
901
|
+
const id = source["childId"];
|
|
902
|
+
const descriptor = source["descriptor"];
|
|
903
|
+
const count = source["descriptorCount"];
|
|
904
|
+
const known = isSessionFormatJsonObject(descriptor) && [
|
|
905
|
+
1,
|
|
906
|
+
2,
|
|
907
|
+
3
|
|
908
|
+
].includes(descriptor["version"]);
|
|
909
|
+
if (count !== 1 || !known) return void 0;
|
|
910
|
+
if (typeof descriptor["provider"] !== "string") throw new SessionFormatUnsupportedMigrationError(`${childCatalogSubject(source)} has an invalid subagent descriptor provider`);
|
|
911
|
+
if (descriptor["version"] !== 1 && descriptor["mode"] !== "continuable" && descriptor["mode"] !== "one-shot") throw new SessionFormatUnsupportedMigrationError(`${childCatalogSubject(source)} has an invalid subagent descriptor mode`);
|
|
912
|
+
return catalogFact({
|
|
913
|
+
version: 0,
|
|
914
|
+
childId: id,
|
|
915
|
+
childCreatedAt: source["childCreatedAt"],
|
|
916
|
+
mode: descriptor["version"] === 1 ? "continuable" : descriptor["mode"],
|
|
917
|
+
...descriptor["label"] === void 0 ? {} : { label: descriptor["label"] }
|
|
918
|
+
}, childCatalogSubject(source));
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Validate the historical fields used for catalog membership without interpreting extensions.
|
|
922
|
+
* @param value - catalog payload or prepared historical discovery fact.
|
|
923
|
+
* @param subject - event type or child identity used in rejection diagnostics.
|
|
924
|
+
* @returns validated catalog payload.
|
|
925
|
+
*/
|
|
926
|
+
function catalogFact(value, subject = "subagent/catalog") {
|
|
927
|
+
if (!isSessionFormatJsonObject(value) || value["version"] !== 0 && value["version"] !== 1 || typeof value["childId"] !== "string" || value["mode"] !== "continuable" && value["mode"] !== "one-shot" && value["mode"] !== "unknown" || value["version"] === 0 && value["mode"] === "unknown" || value["mode"] === "continuable" && typeof value["label"] !== "string" || value["label"] !== void 0 && typeof value["label"] !== "string") throw new SessionFormatError(`${subject} requires a supported versioned catalog fact`);
|
|
928
|
+
sessionFormatCount(value["childCreatedAt"], "catalog child creation time");
|
|
929
|
+
return value;
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Name the child and its optional storage-owned location in a migration diagnostic.
|
|
933
|
+
* @param source - validated supplemental child evidence.
|
|
934
|
+
* @returns a child identity with its source path when persistence supplied one.
|
|
935
|
+
*/
|
|
936
|
+
function childCatalogSubject(source) {
|
|
937
|
+
return `Session ${source["childId"]}${typeof source["sourcePath"] === "string" ? ` (raw log: ${source["sourcePath"]})` : ""}`;
|
|
938
|
+
}
|
|
939
|
+
//#endregion
|
|
940
|
+
//#region lib/types/validation.js
|
|
941
|
+
/** Native V4 metadata and generation-owned relationship validation. */
|
|
942
|
+
/**
|
|
943
|
+
* Validate the exact native V4 logical header.
|
|
944
|
+
* @param header - decoded or otherwise untrusted V4 Session header candidate.
|
|
945
|
+
*/
|
|
946
|
+
function assertReleasedV4Header(header) {
|
|
947
|
+
if (!isSessionFormatJsonObject(header) || header["version"] !== 4) throw new SessionFormatError("expected format v4 header");
|
|
948
|
+
const required = [
|
|
949
|
+
"version",
|
|
950
|
+
"id",
|
|
951
|
+
"createdAt",
|
|
952
|
+
"isSeeded",
|
|
953
|
+
"delegationDepth"
|
|
954
|
+
];
|
|
955
|
+
const allowed = new Set([
|
|
956
|
+
...required,
|
|
957
|
+
"cwd",
|
|
958
|
+
"parentSession",
|
|
959
|
+
"origin",
|
|
960
|
+
"agentPreset"
|
|
961
|
+
]);
|
|
962
|
+
const missing = required.find((key) => !Object.hasOwn(header, key));
|
|
963
|
+
const unexpected = Object.keys(header).find((key) => !allowed.has(key));
|
|
964
|
+
if (missing !== void 0) throw new SessionFormatError(`format v4 header lacks required field ${missing}`);
|
|
965
|
+
if (unexpected !== void 0) throw new SessionFormatError(`format v4 header has unexpected field ${unexpected}`);
|
|
966
|
+
if (typeof header.id !== "string") throw new SessionFormatError("format v4 header id must be a string");
|
|
967
|
+
sessionFormatCount(header.createdAt, "format v4 header createdAt");
|
|
968
|
+
sessionFormatCount(header.delegationDepth, "format v4 header delegationDepth");
|
|
969
|
+
if (typeof header.isSeeded !== "boolean") throw new SessionFormatError("format v4 header isSeeded must be boolean");
|
|
970
|
+
if (header.cwd !== void 0 && (typeof header.cwd !== "string" || !isAbsolute(header.cwd))) throw new SessionFormatError("format v4 header cwd must be absolute");
|
|
971
|
+
for (const key of ["parentSession", "agentPreset"]) if (header[key] !== void 0 && typeof header[key] !== "string") throw new SessionFormatError(`format v4 header ${key} must be a string`);
|
|
972
|
+
if (header.origin !== void 0 && header.origin !== "subagent") throw new SessionFormatError("format v4 header origin must be \"subagent\"");
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* Validate V4 inheritance, vocabulary, native message admission, and
|
|
976
|
+
* lifecycle, compaction, tool, retry, title, command, catalog, and delivery ownership.
|
|
977
|
+
* Installed Session restoration owns common event envelopes and message acceptance.
|
|
978
|
+
* @param artifact - complete detached V4 artifact.
|
|
979
|
+
* @param knownEventTypes - event types understood by the installed Session package.
|
|
980
|
+
* @returns the same validated artifact and event objects.
|
|
981
|
+
*/
|
|
982
|
+
function restoreReleasedV4Artifact(artifact, knownEventTypes) {
|
|
983
|
+
assertReleasedV4Header(artifact.header);
|
|
984
|
+
const cut = sessionFormatCount(artifact.inheritedEventCount, "format v4 inherited event count");
|
|
985
|
+
if (cut > artifact.events.length) throw new SessionFormatError("format v4 inherited event count exceeds its events");
|
|
986
|
+
if (!artifact.header.isSeeded && cut !== 0) throw new SessionFormatError("unseeded format v4 Session has inherited events");
|
|
987
|
+
let lastInheritedMarker;
|
|
988
|
+
for (const [index, event] of artifact.events.entries()) {
|
|
989
|
+
if (!knownEventTypes.has(event.type) && event["ignorable"] !== true) throw new SessionFormatUnsupportedMigrationError(`format v4 contains unknown event type ${JSON.stringify(event.type)} at seq ${index}`);
|
|
990
|
+
if (event.seq !== index) throw new SessionFormatError(`format v4 event ${index} is not dense`);
|
|
991
|
+
if (!knownEventTypes.has(event.type)) continue;
|
|
992
|
+
assertV4RetiredSyntax(event);
|
|
993
|
+
assertV4SystemMessageFields(event);
|
|
994
|
+
assertV4ToolResultMessage(event);
|
|
995
|
+
assertV4ForkResult(event);
|
|
996
|
+
if (event.type === "session/end-seed" && isSessionFormatJsonObject(event.data) && event.data["inherited"] === true) lastInheritedMarker = index;
|
|
997
|
+
}
|
|
998
|
+
if (artifact.header.isSeeded && lastInheritedMarker !== cut) throw new SessionFormatError("format v4 seeded header disagrees with its last inherited end-seed marker");
|
|
999
|
+
if (!artifact.header.isSeeded && lastInheritedMarker !== void 0) throw new SessionFormatError("format v4 unseeded Session contains an inherited end-seed marker");
|
|
1000
|
+
assertReleasedV4Relationships(artifact, knownEventTypes);
|
|
1001
|
+
return artifact;
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Validate delivery generation and active-generation coordinates before evaluating ownership.
|
|
1005
|
+
* @param event - decoded event whose delivery payload may be inspected.
|
|
1006
|
+
* @param currentVersion - generation whose watermark coordinates are active.
|
|
1007
|
+
* @returns the active delivery's nonempty Session id, or undefined for other events and generations.
|
|
1008
|
+
*/
|
|
1009
|
+
function validateDeliveryAccepted(event, currentVersion) {
|
|
1010
|
+
if (event.type !== "session-log-deepseek/delivery-accepted") return void 0;
|
|
1011
|
+
const data = event.data;
|
|
1012
|
+
if (!isSessionFormatJsonObject(data)) throw new SessionFormatError("delivery-accepted data must be an object");
|
|
1013
|
+
if (sessionFormatCount(data["sessionFormatVersion"] === void 0 ? 0 : data["sessionFormatVersion"], "delivery sessionFormatVersion") !== currentVersion) return void 0;
|
|
1014
|
+
if (sessionFormatCount(data["throughSeq"], "delivery throughSeq") >= event.seq) throw new SessionFormatError("delivery throughSeq must precede its marker");
|
|
1015
|
+
const id = data["sessionId"];
|
|
1016
|
+
if (typeof id !== "string" || id.length === 0) throw new SessionFormatError("delivery requires a nonempty Session id");
|
|
1017
|
+
return id;
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Validate native developer fields, message sources, lifecycle, catalog, and delivery
|
|
1021
|
+
* relationships without changing event vocabulary or tail recovery.
|
|
1022
|
+
* The owning admission stage rejects unknown required events;
|
|
1023
|
+
* unknown ignorable records retain their uninterpreted payloads.
|
|
1024
|
+
* @param artifact - decoded artifact with its final inherited cut.
|
|
1025
|
+
* @param knownEventTypes - installed event types whose payloads this reader interprets.
|
|
1026
|
+
*/
|
|
1027
|
+
function assertReleasedV4Relationships(artifact, knownEventTypes) {
|
|
1028
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1029
|
+
for (const event of artifact.events) {
|
|
1030
|
+
if (!knownEventTypes.has(event.type)) continue;
|
|
1031
|
+
assertV4DeveloperData(event);
|
|
1032
|
+
assertV4MessageSources(event);
|
|
1033
|
+
const deliveryId = validateDeliveryAccepted(event, 4);
|
|
1034
|
+
if (deliveryId !== void 0 && !(artifact.header.parentSession !== void 0 && event.seq < artifact.inheritedEventCount) && deliveryId !== artifact.header.id) throw new SessionFormatError("current-generation delivery marker names the wrong Session");
|
|
1035
|
+
if (event.type === "subagent/catalog" && event.seq >= artifact.inheritedEventCount) {
|
|
1036
|
+
const id = catalogFact(event.data)["childId"];
|
|
1037
|
+
if (ids.has(id)) throw new SessionFormatError(`duplicate catalog child ${id}`);
|
|
1038
|
+
ids.add(id);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
assertV4LifecycleRelationships(artifact, knownEventTypes);
|
|
1042
|
+
}
|
|
1043
|
+
//#endregion
|
|
1044
|
+
//#region lib/types/codec.js
|
|
1045
|
+
/** V4 framing with native tool-role admission and released physical rows. */
|
|
1046
|
+
function physicalV2(value) {
|
|
1047
|
+
if (!isSessionFormatJsonObject(value) || value["version"] !== 4) throw new SessionFormatError("expected format v4 physical header");
|
|
1048
|
+
return {
|
|
1049
|
+
...value,
|
|
1050
|
+
version: 2
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* V4 physical encoder and decoder retain the released row framing while
|
|
1055
|
+
* validating the native tool-role message directly.
|
|
1056
|
+
*/
|
|
1057
|
+
const releasedV4SessionFormatCodec = Object.freeze({
|
|
1058
|
+
version: 4,
|
|
1059
|
+
decodeHeader(value) {
|
|
1060
|
+
return {
|
|
1061
|
+
...releasedV2SessionFormatCodec.decodeHeader(physicalV2(value)),
|
|
1062
|
+
version: 4
|
|
1063
|
+
};
|
|
1064
|
+
},
|
|
1065
|
+
createDecoder(value, recovery) {
|
|
1066
|
+
const decoder = releasedV2SessionFormatCodec.createDecoder(physicalV2(value), recovery);
|
|
1067
|
+
return {
|
|
1068
|
+
...decoder,
|
|
1069
|
+
header: {
|
|
1070
|
+
...decoder.header,
|
|
1071
|
+
version: 4
|
|
1072
|
+
},
|
|
1073
|
+
decodeRow(row, context) {
|
|
1074
|
+
assertV4RowAdmission(row);
|
|
1075
|
+
decoder.decodeRow(row, {
|
|
1076
|
+
emitRun: context.emitRun.bind(context),
|
|
1077
|
+
emitEvent: context.emitEvent.bind(context)
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
},
|
|
1082
|
+
encodeHeader(header, inheritedEventCount) {
|
|
1083
|
+
assertReleasedV4Header(header);
|
|
1084
|
+
return {
|
|
1085
|
+
...releasedV2SessionFormatCodec.encodeHeader({
|
|
1086
|
+
...header,
|
|
1087
|
+
version: 2
|
|
1088
|
+
}, inheritedEventCount),
|
|
1089
|
+
version: 4
|
|
1090
|
+
};
|
|
1091
|
+
},
|
|
1092
|
+
encodeEvent(event) {
|
|
1093
|
+
if (event.type === "developer/message" && event["ignorable"] === true) {
|
|
1094
|
+
assertV4DeveloperData(event);
|
|
1095
|
+
assertV4RetiredSyntax(event);
|
|
1096
|
+
}
|
|
1097
|
+
assertV4RowAdmission(event);
|
|
1098
|
+
return releasedV2SessionFormatCodec.encodeEvent(event);
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1101
|
+
/**
|
|
1102
|
+
* Apply native V4 admission before a scanner discards a recoverable suffix.
|
|
1103
|
+
* Ignorable developer payloads require reader vocabulary; physical decoding defers them.
|
|
1104
|
+
* @param row - parsed physical row before framing and source-event range decoding.
|
|
1105
|
+
* @param knownEventTypes - installed event types, supplied by native readers before tail recovery.
|
|
1106
|
+
*/
|
|
1107
|
+
function assertV4RowAdmission(row, knownEventTypes) {
|
|
1108
|
+
if (isSessionFormatJsonObject(row)) {
|
|
1109
|
+
if (row["type"] === "developer/message" && row["ignorable"] === true && knownEventTypes?.has("developer/message") !== true) return;
|
|
1110
|
+
assertV4DeveloperData(row);
|
|
1111
|
+
}
|
|
1112
|
+
assertV4SourceRowAdmission(row);
|
|
1113
|
+
assertV4RetiredSyntax(row);
|
|
1114
|
+
assertV4SystemMessageFields(row);
|
|
1115
|
+
if (!isSessionFormatJsonObject(row) || row["type"] !== "tool/result") return;
|
|
1116
|
+
const event = row;
|
|
1117
|
+
assertV4ToolResultMessage(event);
|
|
1118
|
+
assertV4ForkResult(event);
|
|
1119
|
+
}
|
|
1120
|
+
//#endregion
|
|
1121
|
+
//#region lib/types/content.js
|
|
1122
|
+
/** V3 content tags and request-tool fields admitted before V4 interpretation. */
|
|
1123
|
+
const V3_BLOCK_TYPES = new Set([
|
|
1124
|
+
"text",
|
|
1125
|
+
"reasoning",
|
|
1126
|
+
"image",
|
|
1127
|
+
"file",
|
|
1128
|
+
"tool-call",
|
|
1129
|
+
"tool-result"
|
|
1130
|
+
]);
|
|
1131
|
+
function migrateBlock(value, subject) {
|
|
1132
|
+
if (!isSessionFormatJsonObject(value) || typeof value["type"] !== "string") throw new SessionFormatError(`${subject} requires content blocks with string type tags`);
|
|
1133
|
+
const type = value["type"];
|
|
1134
|
+
if (V3_BLOCK_TYPES.has(type)) return value;
|
|
1135
|
+
return {
|
|
1136
|
+
...value,
|
|
1137
|
+
type: `plugin:${type}`
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Convert declared content tags while keeping arguments and extension data opaque.
|
|
1142
|
+
* @param value - released content array.
|
|
1143
|
+
* @param subject - owning event and content location for diagnostics.
|
|
1144
|
+
* @returns content in the same order, sharing unchanged blocks and image references.
|
|
1145
|
+
*/
|
|
1146
|
+
function migrateV3Content(value, subject) {
|
|
1147
|
+
if (!Array.isArray(value)) throw new SessionFormatError(`${subject} content must be an array`);
|
|
1148
|
+
const mapped = value.map((block, index) => migrateBlock(block, `${subject}[${index}]`));
|
|
1149
|
+
return mapped.every((block, index) => block === value[index]) ? value : mapped;
|
|
1150
|
+
}
|
|
1151
|
+
function migrateMessage(message, subject) {
|
|
1152
|
+
const content = migrateV3Content(message["content"], subject);
|
|
1153
|
+
return content === message["content"] ? message : {
|
|
1154
|
+
...message,
|
|
1155
|
+
content
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
function migrateChunk(value, subject) {
|
|
1159
|
+
if (!isSessionFormatJsonObject(value)) return value;
|
|
1160
|
+
if (value["type"] === "block-end") {
|
|
1161
|
+
const block = migrateBlock(value["block"], `${subject}.block`);
|
|
1162
|
+
return block === value["block"] ? value : {
|
|
1163
|
+
...value,
|
|
1164
|
+
block
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
if (value["type"] !== "block-start") return value;
|
|
1168
|
+
const original = value["blockType"];
|
|
1169
|
+
if (typeof original !== "string") throw new SessionFormatError(`${subject} blockType must be a string`);
|
|
1170
|
+
const blockType = V3_BLOCK_TYPES.has(original) ? original : `plugin:${original}`;
|
|
1171
|
+
return blockType === original ? value : {
|
|
1172
|
+
...value,
|
|
1173
|
+
blockType
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Convert declared V3 extension content and stream tags after canonical result lifting.
|
|
1178
|
+
* @param event - source event after producer attribution conversion.
|
|
1179
|
+
* @returns the event with namespaced content tags, retaining all coordinates and other fields.
|
|
1180
|
+
* @throws {SessionFormatUnsupportedMigrationError} A V3 tool definition contains the V4-only deferLoading field.
|
|
1181
|
+
*/
|
|
1182
|
+
function migrateV3EventContent(event) {
|
|
1183
|
+
const subject = `format v3 ${event.type} at seq ${event.seq}`;
|
|
1184
|
+
let mapped = mapEventMessages(event, (message) => migrateMessage(message, subject));
|
|
1185
|
+
if (!isSessionFormatJsonObject(mapped.data)) return mapped;
|
|
1186
|
+
let data = mapped.data;
|
|
1187
|
+
const contentField = (key) => {
|
|
1188
|
+
const content = migrateV3Content(data[key], `${subject}.${key}`);
|
|
1189
|
+
if (content !== data[key]) data = {
|
|
1190
|
+
...data,
|
|
1191
|
+
[key]: content
|
|
1192
|
+
};
|
|
1193
|
+
};
|
|
1194
|
+
if (event.type === "compaction/summary") {
|
|
1195
|
+
contentField("summary");
|
|
1196
|
+
if (data["rawOutput"] !== void 0) contentField("rawOutput");
|
|
1197
|
+
} else if (event.type === "tool/ptc-dispatch") contentField("content");
|
|
1198
|
+
else if (event.type === "team/message/queued" && isSessionFormatJsonObject(data["message"])) {
|
|
1199
|
+
const message = migrateMessage(data["message"], subject);
|
|
1200
|
+
if (message !== data["message"]) data = {
|
|
1201
|
+
...data,
|
|
1202
|
+
message
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
if (event.type === "request/header" && isSessionFormatJsonObject(data["header"])) {
|
|
1206
|
+
const tools = data["header"]["tools"];
|
|
1207
|
+
if (Array.isArray(tools)) {
|
|
1208
|
+
for (const [index, tool] of tools.entries()) if (isSessionFormatJsonObject(tool) && Object.hasOwn(tool, "deferLoading")) throw new SessionFormatUnsupportedMigrationError(`${subject}.header.tools[${index}] contains deferLoading, which is only defined in V4`);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
if ((event.type === "assistant/message" || event.type === "assistant/attempt") && Array.isArray(data["stream"])) {
|
|
1212
|
+
const stream = data["stream"];
|
|
1213
|
+
const converted = stream.map((entry, index) => {
|
|
1214
|
+
if (!isSessionFormatJsonObject(entry) || entry["type"] !== "chunk") return entry;
|
|
1215
|
+
const chunk = migrateChunk(entry["chunk"], `${subject}.stream[${index}]`);
|
|
1216
|
+
return chunk === entry["chunk"] ? entry : {
|
|
1217
|
+
...entry,
|
|
1218
|
+
chunk
|
|
1219
|
+
};
|
|
1220
|
+
});
|
|
1221
|
+
if (converted.some((entry, index) => entry !== stream[index])) data = {
|
|
1222
|
+
...data,
|
|
1223
|
+
stream: converted
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
if (data !== mapped.data) mapped = {
|
|
1227
|
+
...mapped,
|
|
1228
|
+
data
|
|
1229
|
+
};
|
|
1230
|
+
return mapped;
|
|
1231
|
+
}
|
|
1232
|
+
//#endregion
|
|
1233
|
+
//#region lib/types/extension-identities.js
|
|
1234
|
+
/** Fixed V3 event vocabulary and namespaced historical opaque events. */
|
|
1235
|
+
/** First-party event names understood by the released V3 reader, independent of the installed writer. */
|
|
1236
|
+
const RELEASED_V3_EVENT_TYPES = new Set([
|
|
1237
|
+
"agent-preset/selected",
|
|
1238
|
+
"agent/inbox/spliced",
|
|
1239
|
+
"approval/asked",
|
|
1240
|
+
"approval/decided",
|
|
1241
|
+
"approval/policy",
|
|
1242
|
+
"assistant/attempt",
|
|
1243
|
+
"assistant/message",
|
|
1244
|
+
"command/done",
|
|
1245
|
+
"command/run",
|
|
1246
|
+
"compaction/end",
|
|
1247
|
+
"compaction/prune",
|
|
1248
|
+
"compaction/start",
|
|
1249
|
+
"compaction/summary",
|
|
1250
|
+
"deliverables/presented",
|
|
1251
|
+
"feedback/message-delete",
|
|
1252
|
+
"feedback/message-put",
|
|
1253
|
+
"feedback/record",
|
|
1254
|
+
"goal/change",
|
|
1255
|
+
"hook/invoked",
|
|
1256
|
+
"hook/result",
|
|
1257
|
+
"image/offload",
|
|
1258
|
+
"llm/retry",
|
|
1259
|
+
"llm/retry-started",
|
|
1260
|
+
"model/selection",
|
|
1261
|
+
"permission/preset",
|
|
1262
|
+
"plan/mode",
|
|
1263
|
+
"request/context",
|
|
1264
|
+
"request/header",
|
|
1265
|
+
"sandbox/mode",
|
|
1266
|
+
"schedule/change",
|
|
1267
|
+
"session-log-deepseek/delivery-accepted",
|
|
1268
|
+
"session/end-seed",
|
|
1269
|
+
"session/title",
|
|
1270
|
+
"session/title-llm-request",
|
|
1271
|
+
"step/end",
|
|
1272
|
+
"step/start",
|
|
1273
|
+
"subagent/catalog",
|
|
1274
|
+
"subagent/descriptor",
|
|
1275
|
+
"subagent/model-selection-policy",
|
|
1276
|
+
"system/message",
|
|
1277
|
+
"team/member",
|
|
1278
|
+
"team/message/delivered",
|
|
1279
|
+
"team/message/queued",
|
|
1280
|
+
"team/task",
|
|
1281
|
+
"todo/write",
|
|
1282
|
+
"tool-workflow/agent-end",
|
|
1283
|
+
"tool-workflow/agent-start",
|
|
1284
|
+
"tool-workflow/run-end",
|
|
1285
|
+
"tool-workflow/run-start",
|
|
1286
|
+
"tool/call",
|
|
1287
|
+
"tool/ptc-dispatch",
|
|
1288
|
+
"tool/ptc-dispatch-start",
|
|
1289
|
+
"tool/result",
|
|
1290
|
+
"turn/end",
|
|
1291
|
+
"turn/start",
|
|
1292
|
+
"user/message",
|
|
1293
|
+
"web/deepseek-search-llm-request",
|
|
1294
|
+
"workspace/changes"
|
|
1295
|
+
]);
|
|
1296
|
+
/**
|
|
1297
|
+
* Keep unknown ignorable events opaque after header promotion.
|
|
1298
|
+
* @param event - original V3 event; this incoming identity conversion is applied once.
|
|
1299
|
+
* @returns the same event or an ignorable namespaced event retaining its payload and coordinates.
|
|
1300
|
+
*/
|
|
1301
|
+
function namespaceV3OpaqueEvent(event) {
|
|
1302
|
+
return event["ignorable"] === true && !RELEASED_V3_EVENT_TYPES.has(event.type) ? {
|
|
1303
|
+
...event,
|
|
1304
|
+
type: `plugin:${event.type}`,
|
|
1305
|
+
ignorable: true
|
|
1306
|
+
} : event;
|
|
1307
|
+
}
|
|
1308
|
+
//#endregion
|
|
1309
|
+
//#region lib/types/references.js
|
|
1310
|
+
/** Remap released V3 local event references after interrupted-turn insertion. */
|
|
1311
|
+
function object(value) {
|
|
1312
|
+
if (!isSessionFormatJsonObject(value)) throw new SessionFormatError("V3 event reference container must be an object");
|
|
1313
|
+
return value;
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Remap first-party local references, retaining generation-qualified captures and numeric payloads.
|
|
1317
|
+
* @param event - first-party source event before envelope renumbering.
|
|
1318
|
+
* @param seq - target event position.
|
|
1319
|
+
* @param mapping - target positions of all earlier source events.
|
|
1320
|
+
* @returns the event with target coordinates; unchanged events retain their identity.
|
|
1321
|
+
*/
|
|
1322
|
+
function remapV3References(event, seq, mapping) {
|
|
1323
|
+
if (seq === event.seq) return event;
|
|
1324
|
+
const reference = (value) => {
|
|
1325
|
+
const source = sessionFormatCount(value, "V3 source event reference");
|
|
1326
|
+
const target = mapping[source];
|
|
1327
|
+
if (source >= event.seq || target === void 0) throw new SessionFormatError("V3 reference must name an earlier source event");
|
|
1328
|
+
return target;
|
|
1329
|
+
};
|
|
1330
|
+
const references = (value) => {
|
|
1331
|
+
if (!Array.isArray(value)) throw new SessionFormatError("V3 event references must be an array");
|
|
1332
|
+
return value.map(reference);
|
|
1333
|
+
};
|
|
1334
|
+
let data = event.data;
|
|
1335
|
+
switch (event.type) {
|
|
1336
|
+
case "command/done": {
|
|
1337
|
+
const source = object(data);
|
|
1338
|
+
if (source["sourceEventSeq"] !== void 0) data = {
|
|
1339
|
+
...source,
|
|
1340
|
+
sourceEventSeq: reference(source["sourceEventSeq"])
|
|
1341
|
+
};
|
|
1342
|
+
break;
|
|
1343
|
+
}
|
|
1344
|
+
case "compaction/summary":
|
|
1345
|
+
case "compaction/prune": {
|
|
1346
|
+
const source = object(data);
|
|
1347
|
+
const range = object(source["shadowedRange"]);
|
|
1348
|
+
data = {
|
|
1349
|
+
...source,
|
|
1350
|
+
shadowedRange: {
|
|
1351
|
+
...range,
|
|
1352
|
+
start: reference(range["start"]),
|
|
1353
|
+
end: reference(range["end"])
|
|
1354
|
+
},
|
|
1355
|
+
shadowedSeqs: references(source["shadowedSeqs"])
|
|
1356
|
+
};
|
|
1357
|
+
break;
|
|
1358
|
+
}
|
|
1359
|
+
case "session/title":
|
|
1360
|
+
case "session/title-llm-request": {
|
|
1361
|
+
const source = object(data);
|
|
1362
|
+
data = {
|
|
1363
|
+
...source,
|
|
1364
|
+
messageSeqs: references(source["messageSeqs"])
|
|
1365
|
+
};
|
|
1366
|
+
break;
|
|
1367
|
+
}
|
|
1368
|
+
case "image/offload": {
|
|
1369
|
+
const source = object(data);
|
|
1370
|
+
const targets = source["targets"];
|
|
1371
|
+
if (!Array.isArray(targets)) throw new SessionFormatError("V3 image offload targets must be an array");
|
|
1372
|
+
data = {
|
|
1373
|
+
...source,
|
|
1374
|
+
targets: targets.map((value) => {
|
|
1375
|
+
const target = object(value);
|
|
1376
|
+
return {
|
|
1377
|
+
...target,
|
|
1378
|
+
seq: reference(target["seq"])
|
|
1379
|
+
};
|
|
1380
|
+
})
|
|
1381
|
+
};
|
|
1382
|
+
break;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
const surface = event["surfaceOp"];
|
|
1386
|
+
const range = surface === void 0 || surface === "append" ? void 0 : object(surface);
|
|
1387
|
+
return {
|
|
1388
|
+
...event,
|
|
1389
|
+
seq,
|
|
1390
|
+
data,
|
|
1391
|
+
...event["sourceEventSeqs"] === void 0 ? {} : { sourceEventSeqs: references(event["sourceEventSeqs"]) },
|
|
1392
|
+
...range === void 0 ? {} : { surfaceOp: {
|
|
1393
|
+
...range,
|
|
1394
|
+
startSeq: reference(range["startSeq"]),
|
|
1395
|
+
endSeq: reference(range["endSeq"])
|
|
1396
|
+
} }
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
//#endregion
|
|
1400
|
+
//#region lib/types/migration.js
|
|
1401
|
+
/** Append historical child facts after converting V3 source events to V4. */
|
|
1402
|
+
/** Header-only migration declaration; body restoration requires explicit child evidence. */
|
|
1403
|
+
const sessionFormatV3ToV4 = defineSessionFormatMigration({
|
|
1404
|
+
name: "@deepseek-ai/dsh-session-format-v3-to-v4",
|
|
1405
|
+
fromVersion: 3,
|
|
1406
|
+
toVersion: 4,
|
|
1407
|
+
migrateHeader(header) {
|
|
1408
|
+
assertReleasedV3Header(header);
|
|
1409
|
+
return {
|
|
1410
|
+
...header,
|
|
1411
|
+
version: 4
|
|
1412
|
+
};
|
|
1413
|
+
},
|
|
1414
|
+
createStage() {
|
|
1415
|
+
throw new SessionFormatUnsupportedMigrationError("V3 catalog migration requires explicit historical child facts, including an empty array for a parent without children");
|
|
1416
|
+
},
|
|
1417
|
+
validateTargetHeader: assertReleasedV4Header
|
|
1418
|
+
});
|
|
1419
|
+
/**
|
|
1420
|
+
* Bind one parent's historical child evidence to its V3→V4 migration.
|
|
1421
|
+
* @param children - complete child evidence retained unchanged for the lifetime of this declaration; an empty array declares no children.
|
|
1422
|
+
* @returns an adjacent migration that creates independent stages with the supplied evidence.
|
|
1423
|
+
*/
|
|
1424
|
+
function createSessionFormatV3ToV4(children) {
|
|
1425
|
+
return defineSessionFormatMigration({
|
|
1426
|
+
...sessionFormatV3ToV4,
|
|
1427
|
+
createStage: (input) => new ReleasedV3ToV4Stage(input, children)
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
var ReleasedV3ToV4Stage = class {
|
|
1431
|
+
input;
|
|
1432
|
+
headerInheritedEventCount;
|
|
1433
|
+
candidates;
|
|
1434
|
+
catalogs = [];
|
|
1435
|
+
cut;
|
|
1436
|
+
sourceCut;
|
|
1437
|
+
mapping = [];
|
|
1438
|
+
turn;
|
|
1439
|
+
stepOpen = false;
|
|
1440
|
+
nextTurnSpliced = false;
|
|
1441
|
+
nextSeq = 0;
|
|
1442
|
+
time;
|
|
1443
|
+
foreignDeliverySeq;
|
|
1444
|
+
constructor(input, children) {
|
|
1445
|
+
this.input = input;
|
|
1446
|
+
this.candidates = children.map(childCatalogSource).sort((left, right) => left["childCreatedAt"] - right["childCreatedAt"] || (left["childId"] === right["childId"] ? 0 : left["childId"] < right["childId"] ? -1 : 1));
|
|
1447
|
+
this.cut = input.sourceHeader.isSeeded ? void 0 : 0;
|
|
1448
|
+
this.sourceCut = this.cut;
|
|
1449
|
+
if (!input.sourceHeader.isSeeded) this.headerInheritedEventCount = 0;
|
|
1450
|
+
this.time = input.sourceHeader.createdAt;
|
|
1451
|
+
}
|
|
1452
|
+
transformEvent(event, context) {
|
|
1453
|
+
if (event.seq !== this.mapping.length) throw new SessionFormatError("V3 source events must be dense");
|
|
1454
|
+
const interrupted = this.observeRestart(event);
|
|
1455
|
+
if (interrupted !== void 0) context.emitEvent({
|
|
1456
|
+
type: "turn/end",
|
|
1457
|
+
seq: this.nextSeq++,
|
|
1458
|
+
time: event.time,
|
|
1459
|
+
data: {
|
|
1460
|
+
turn: interrupted,
|
|
1461
|
+
reason: { kind: "interrupted" }
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
const targetSeq = this.nextSeq++;
|
|
1465
|
+
this.time = event.time;
|
|
1466
|
+
if (event.type === "session/end-seed" && isSessionFormatJsonObject(event.data) && event.data["inherited"] === true) {
|
|
1467
|
+
if (!this.input.sourceHeader.isSeeded) throw new SessionFormatError("unseeded format v3 Session contains an inherited end-seed marker");
|
|
1468
|
+
this.sourceCut = event.seq;
|
|
1469
|
+
this.cut = targetSeq;
|
|
1470
|
+
this.catalogs.length = 0;
|
|
1471
|
+
} else if (event.type === "subagent/catalog") this.catalogs.push(event.data);
|
|
1472
|
+
const deliveryId = validateDeliveryAccepted(event, 3);
|
|
1473
|
+
if (event.type === "session-log-deepseek/delivery-accepted") {
|
|
1474
|
+
if (event.data["sessionFormatVersion"] === 4) throw new SessionFormatUnsupportedMigrationError("format v3 delivery marker claims target format v4");
|
|
1475
|
+
if (deliveryId !== void 0 && deliveryId !== this.input.sourceHeader.id) this.foreignDeliverySeq = event.seq;
|
|
1476
|
+
}
|
|
1477
|
+
const opaque = namespaceV3OpaqueEvent(event);
|
|
1478
|
+
if (opaque !== event) {
|
|
1479
|
+
this.mapping.push(targetSeq);
|
|
1480
|
+
context.emitEvent(opaque.seq === targetSeq ? opaque : {
|
|
1481
|
+
...opaque,
|
|
1482
|
+
seq: targetSeq
|
|
1483
|
+
});
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
if (!RELEASED_V3_EVENT_TYPES.has(event.type)) throw new SessionFormatUnsupportedMigrationError(`format v3 contains unknown event type ${JSON.stringify(event.type)} at seq ${event.seq}`);
|
|
1487
|
+
const remapped = remapV3References(event, targetSeq, this.mapping);
|
|
1488
|
+
this.mapping.push(targetSeq);
|
|
1489
|
+
const rewritten = mapEventMessages(remapped, (message) => {
|
|
1490
|
+
const source = message["source"];
|
|
1491
|
+
if (!isSessionFormatJsonObject(source)) return message;
|
|
1492
|
+
const converted = rewriteV3MessageSource(source, event.seq, message["role"]);
|
|
1493
|
+
return converted === source ? message : {
|
|
1494
|
+
...message,
|
|
1495
|
+
source: converted
|
|
1496
|
+
};
|
|
1497
|
+
});
|
|
1498
|
+
context.emitEvent(migrateV3EventContent(liftToolResult(rewritten)));
|
|
1499
|
+
}
|
|
1500
|
+
transformRun(run, context) {
|
|
1501
|
+
for (const event of run.expand()) this.transformEvent(event, context);
|
|
1502
|
+
}
|
|
1503
|
+
observeRestart(event) {
|
|
1504
|
+
const data = event.data;
|
|
1505
|
+
const interrupted = event.type === "turn/start" && this.turn !== void 0 && !this.stepOpen && this.nextTurnSpliced && isSessionFormatJsonObject(data) && data["turn"] === this.turn + 1 ? this.turn : void 0;
|
|
1506
|
+
this.nextTurnSpliced = event.type === "agent/inbox/spliced" && isSessionFormatJsonObject(data) && data["target"] === "next-turn" && Array.isArray(data["inserted"]) && data["inserted"].length > 0;
|
|
1507
|
+
if (event.type === "turn/start" && isSessionFormatJsonObject(data) && typeof data["turn"] === "number") this.turn = data["turn"];
|
|
1508
|
+
else if (event.type === "turn/end") this.turn = void 0;
|
|
1509
|
+
else if (event.type === "step/start") this.stepOpen = true;
|
|
1510
|
+
else if (event.type === "step/end") this.stepOpen = false;
|
|
1511
|
+
return interrupted;
|
|
1512
|
+
}
|
|
1513
|
+
finish(context) {
|
|
1514
|
+
const cut = sessionFormatCount(this.cut, "V3 inherited event count");
|
|
1515
|
+
const sourceCut = sessionFormatCount(this.sourceCut, "V3 source inherited event count");
|
|
1516
|
+
const existingCatalogs = /* @__PURE__ */ new Map();
|
|
1517
|
+
for (const data of this.catalogs) {
|
|
1518
|
+
const fact = catalogFact(data);
|
|
1519
|
+
const id = fact["childId"];
|
|
1520
|
+
if (existingCatalogs.has(id)) throw new SessionFormatUnsupportedMigrationError(`duplicate catalog child ${id}`);
|
|
1521
|
+
existingCatalogs.set(id, fact);
|
|
1522
|
+
}
|
|
1523
|
+
if (this.input.sourceInheritedEventCount !== void 0 && sourceCut !== this.input.sourceInheritedEventCount) throw new SessionFormatError("format v3 inherited cut disagrees with its source marker");
|
|
1524
|
+
if (this.foreignDeliverySeq !== void 0 && (this.input.sourceHeader.parentSession === void 0 || this.foreignDeliverySeq >= sourceCut)) throw new SessionFormatError("current-generation delivery marker names the wrong Session");
|
|
1525
|
+
for (const source of this.candidates) {
|
|
1526
|
+
const id = source["childId"];
|
|
1527
|
+
const existing = existingCatalogs.get(id);
|
|
1528
|
+
const fact = childCatalogFact(source);
|
|
1529
|
+
if (existing !== void 0) {
|
|
1530
|
+
if (existing["childCreatedAt"] !== source["childCreatedAt"]) throw new SessionFormatUnsupportedMigrationError(`${childCatalogSubject(source)} conflicts with its parent catalog`);
|
|
1531
|
+
if (fact !== void 0 && ["mode", "label"].some((key) => existing[key] !== fact[key])) throw new SessionFormatUnsupportedMigrationError(`${childCatalogSubject(source)} conflicts with its parent catalog`);
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
const entry = fact ?? {
|
|
1535
|
+
version: 1,
|
|
1536
|
+
childId: id,
|
|
1537
|
+
childCreatedAt: source["childCreatedAt"],
|
|
1538
|
+
mode: "unknown"
|
|
1539
|
+
};
|
|
1540
|
+
existingCatalogs.set(id, entry);
|
|
1541
|
+
context.emitEvent({
|
|
1542
|
+
type: "subagent/catalog",
|
|
1543
|
+
seq: this.nextSeq++,
|
|
1544
|
+
time: this.time,
|
|
1545
|
+
data: entry
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
return cut;
|
|
1549
|
+
}
|
|
1550
|
+
};
|
|
1551
|
+
//#endregion
|
|
1552
|
+
export { RELEASED_V3_EVENT_TYPES, assertReleasedV4Header, assertReleasedV4Relationships, assertV4RowAdmission, createSessionFormatV3ToV4, historicalChildCatalogSource, releasedV3SessionFormatCodec, releasedV4SessionFormatCodec, restoreReleasedV4Artifact, sessionFormatV3ToV4 };
|