session-steward 0.3.0 → 0.5.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/CHANGELOG.md +31 -0
- package/README.md +27 -14
- package/bin/session-steward-cli.mjs +14 -0
- package/dist/assets/index-B6C5qvGV.js +9 -0
- package/dist/assets/index-B8U6eLCA.css +2 -0
- package/dist/index.html +2 -2
- package/lib/cli.mjs +139 -2
- package/lib/providers/claude-code/events.mjs +498 -0
- package/lib/providers/claude-code/index.mjs +2 -0
- package/lib/providers/claude-code/store.mjs +60 -15
- package/lib/providers/codex/database-families.mjs +177 -0
- package/lib/providers/codex/events.mjs +731 -0
- package/lib/providers/codex/index.mjs +2 -0
- package/lib/providers/codex/store.mjs +466 -329
- package/lib/server.mjs +70 -4
- package/lib/session-event-reader.mjs +126 -0
- package/lib/session-events.mjs +307 -0
- package/lib/storage/jsonl.mjs +148 -0
- package/package.json +7 -2
- package/dist/assets/index-CN9iax_v.css +0 -2
- package/dist/assets/index-fUX3qen0.js +0 -9
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSessionEvent,
|
|
3
|
+
createSessionEventCoverage,
|
|
4
|
+
createSessionEventHeader,
|
|
5
|
+
createSessionEventsResult,
|
|
6
|
+
SESSION_EVENT_KIND,
|
|
7
|
+
SESSION_EVENT_REASON,
|
|
8
|
+
} from "../../session-events.mjs";
|
|
9
|
+
import {
|
|
10
|
+
createSessionEventReadState,
|
|
11
|
+
createUnmappedSessionEventTracker,
|
|
12
|
+
isInjectedSessionAsk,
|
|
13
|
+
} from "../../session-event-reader.mjs";
|
|
14
|
+
import { visitJsonlSnapshotEntries } from "../../storage/jsonl.mjs";
|
|
15
|
+
import { getSessionRecord, loadSessionStore } from "./store.mjs";
|
|
16
|
+
|
|
17
|
+
const PROVIDER_ID = "codex";
|
|
18
|
+
const RECORD_CLASSIFICATION = Object.freeze({
|
|
19
|
+
RECOGNIZED: "recognized",
|
|
20
|
+
SKIPPED: "skipped",
|
|
21
|
+
UNMAPPED: "unmapped",
|
|
22
|
+
UNPARSEABLE: "unparseable",
|
|
23
|
+
});
|
|
24
|
+
const SKIPPED_PAYLOAD_TYPES = new Set([
|
|
25
|
+
"agent_reasoning",
|
|
26
|
+
"reasoning",
|
|
27
|
+
"token_count",
|
|
28
|
+
]);
|
|
29
|
+
const SKIPPED_RECORD_TYPES = new Set([
|
|
30
|
+
"inter_agent_communication_metadata",
|
|
31
|
+
"world_state",
|
|
32
|
+
]);
|
|
33
|
+
const DUPLICATE_TEXT_WINDOW = 8;
|
|
34
|
+
const DUPLICATE_TEXT_TRACKED = 64;
|
|
35
|
+
|
|
36
|
+
function createDuplicateTextTracker() {
|
|
37
|
+
const seen = new Map();
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
isDuplicate(kind, text, sequence) {
|
|
41
|
+
const key = `${kind}\u0000${text.replace(/\s+/gu, " ").trim().slice(0, 400)}`;
|
|
42
|
+
const previous = seen.get(key);
|
|
43
|
+
seen.set(key, sequence);
|
|
44
|
+
if (seen.size > DUPLICATE_TEXT_TRACKED) {
|
|
45
|
+
seen.delete(seen.keys().next().value);
|
|
46
|
+
}
|
|
47
|
+
return previous !== undefined && sequence - previous <= DUPLICATE_TEXT_WINDOW;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function asTimestamp(value) {
|
|
53
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
54
|
+
if (typeof value !== "string") return null;
|
|
55
|
+
const timestamp = Date.parse(value);
|
|
56
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function contentText(value) {
|
|
60
|
+
if (typeof value === "string") return value;
|
|
61
|
+
|
|
62
|
+
if (Array.isArray(value)) {
|
|
63
|
+
return value
|
|
64
|
+
.map((part) => contentText(part))
|
|
65
|
+
.filter((part) => part !== "")
|
|
66
|
+
.join("\n");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!value || typeof value !== "object") return "";
|
|
70
|
+
if (typeof value.text === "string") return value.text;
|
|
71
|
+
if (typeof value.content === "string" || Array.isArray(value.content)) {
|
|
72
|
+
return contentText(value.content);
|
|
73
|
+
}
|
|
74
|
+
if (typeof value.message === "string" || Array.isArray(value.message)) {
|
|
75
|
+
return contentText(value.message);
|
|
76
|
+
}
|
|
77
|
+
if (typeof value.summary === "string") return value.summary;
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseArguments(value) {
|
|
82
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return value;
|
|
83
|
+
if (typeof value !== "string") return null;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(value);
|
|
87
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readJavaScriptString(source, start) {
|
|
94
|
+
const quote = source[start];
|
|
95
|
+
if (!['"', "'", "`"].includes(quote)) return null;
|
|
96
|
+
let result = "";
|
|
97
|
+
|
|
98
|
+
for (let index = start + 1; index < source.length; index += 1) {
|
|
99
|
+
const character = source[index];
|
|
100
|
+
|
|
101
|
+
if (character === quote) return result;
|
|
102
|
+
|
|
103
|
+
if (character !== "\\") {
|
|
104
|
+
result += character;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
index += 1;
|
|
109
|
+
if (index >= source.length) return null;
|
|
110
|
+
const escaped = source[index];
|
|
111
|
+
result += {
|
|
112
|
+
n: "\n",
|
|
113
|
+
r: "\r",
|
|
114
|
+
t: "\t",
|
|
115
|
+
}[escaped] ?? escaped;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function javascriptProperty(source, property) {
|
|
122
|
+
if (typeof source !== "string") return null;
|
|
123
|
+
const match = new RegExp(`\\b${property}\\s*:\\s*`, "u").exec(source);
|
|
124
|
+
if (!match) return null;
|
|
125
|
+
return readJavaScriptString(source, match.index + match[0].length);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function embeddedJsonProperty(source, property) {
|
|
129
|
+
if (typeof source !== "string") return null;
|
|
130
|
+
const match = new RegExp(`"${property}"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"`, "u").exec(source);
|
|
131
|
+
if (!match) return null;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const value = JSON.parse(`"${match[1]}"`);
|
|
135
|
+
return typeof value === "string" ? value : null;
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function toolArguments(payload) {
|
|
142
|
+
const parsed = parseArguments(payload.arguments ?? payload.input);
|
|
143
|
+
if (parsed) return parsed;
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
cmd: embeddedJsonProperty(payload.input, "cmd")
|
|
147
|
+
?? javascriptProperty(payload.input, "cmd"),
|
|
148
|
+
workdir: embeddedJsonProperty(payload.input, "workdir")
|
|
149
|
+
?? javascriptProperty(payload.input, "workdir"),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function nestedToolName(source) {
|
|
154
|
+
if (typeof source !== "string") return null;
|
|
155
|
+
const direct = /\btools\.([A-Za-z][\w]*)\s*\(/u.exec(source)?.[1];
|
|
156
|
+
if (direct) return direct;
|
|
157
|
+
return /\bx\.name\s*===\s*"([A-Za-z][\w]*)"/u.exec(source)?.[1] ?? null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function nestedToolLabel(name) {
|
|
161
|
+
if (!name) return null;
|
|
162
|
+
if (name === "exec_command") return null;
|
|
163
|
+
if (name.startsWith("mcp__")) {
|
|
164
|
+
const [server, ...toolParts] = name.slice(5).split("__");
|
|
165
|
+
return `mcp: ${server}/${toolParts.join("/") || "unknown tool"}`;
|
|
166
|
+
}
|
|
167
|
+
if (name.includes("__")) return `tool: ${name.replaceAll("__", "/")}`;
|
|
168
|
+
return `tool: ${name}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function changePaths(changes) {
|
|
172
|
+
if (!changes || typeof changes !== "object" || Array.isArray(changes)) return [];
|
|
173
|
+
return Object.keys(changes).filter(Boolean);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function patchResultFiles(payload) {
|
|
177
|
+
const fromChanges = changePaths(payload.changes);
|
|
178
|
+
if (fromChanges.length > 0) return fromChanges;
|
|
179
|
+
if (typeof payload.stdout !== "string") return [];
|
|
180
|
+
return [...payload.stdout.matchAll(/^[MAD]\s+(.+)$/gmu)]
|
|
181
|
+
.map((match) => match[1].trim())
|
|
182
|
+
.filter(Boolean);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function patchResultDetails(payload) {
|
|
186
|
+
return {
|
|
187
|
+
added: null,
|
|
188
|
+
applied: typeof payload.success === "boolean"
|
|
189
|
+
? payload.success
|
|
190
|
+
: typeof payload.stdout === "string"
|
|
191
|
+
? payload.stdout.startsWith("Success.")
|
|
192
|
+
: null,
|
|
193
|
+
files: patchResultFiles(payload),
|
|
194
|
+
removed: null,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function patchDetails(input) {
|
|
199
|
+
if (typeof input !== "string") {
|
|
200
|
+
return { added: null, files: [], removed: null };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const files = [];
|
|
204
|
+
const seen = new Set();
|
|
205
|
+
const pattern = /^\*\*\* (?:Update|Add|Delete) File: (.+)$/gmu;
|
|
206
|
+
let match;
|
|
207
|
+
|
|
208
|
+
while ((match = pattern.exec(input))) {
|
|
209
|
+
const file = match[1].trim();
|
|
210
|
+
if (!file || seen.has(file)) continue;
|
|
211
|
+
seen.add(file);
|
|
212
|
+
files.push(file);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
added: (input.match(/^\+/gmu) ?? []).length,
|
|
217
|
+
files,
|
|
218
|
+
removed: (input.match(/^-/gmu) ?? []).length,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function commandFailure(payload) {
|
|
223
|
+
if (typeof payload.is_error === "boolean") return payload.is_error;
|
|
224
|
+
if (Number.isInteger(payload.exit_code)) return payload.exit_code !== 0;
|
|
225
|
+
const output = contentText(payload.output ?? payload.content ?? payload.stdout);
|
|
226
|
+
if (/^Script completed(?:\n|$)/u.test(output)) return false;
|
|
227
|
+
if (/^Script failed(?:\n|$)/u.test(output)) return true;
|
|
228
|
+
const match = /(?:Process exited with code|exit[_ ]code["']?\s*[:=]?|Exit code)\s*(-?\d+)/iu.exec(output);
|
|
229
|
+
return match ? Number(match[1]) !== 0 : null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function mcpOutcome(payload) {
|
|
233
|
+
const result = payload.result?.Ok ?? payload.result?.Err ?? payload.result;
|
|
234
|
+
const failed = payload.result?.Err !== undefined
|
|
235
|
+
? true
|
|
236
|
+
: typeof result?.isError === "boolean"
|
|
237
|
+
? result.isError
|
|
238
|
+
: typeof result?.is_error === "boolean"
|
|
239
|
+
? result.is_error
|
|
240
|
+
: null;
|
|
241
|
+
return {
|
|
242
|
+
error: failed === true
|
|
243
|
+
? contentText(result?.content ?? result?.error ?? result).trim() || "Tool failed."
|
|
244
|
+
: null,
|
|
245
|
+
failed,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function mcpToolEvent(payload, atMs, sequence) {
|
|
250
|
+
const invocation = payload.invocation && typeof payload.invocation === "object"
|
|
251
|
+
? payload.invocation
|
|
252
|
+
: {};
|
|
253
|
+
const input = invocation.arguments && typeof invocation.arguments === "object"
|
|
254
|
+
? invocation.arguments
|
|
255
|
+
: {};
|
|
256
|
+
const server = typeof invocation.server === "string" ? invocation.server : "unknown server";
|
|
257
|
+
const tool = typeof invocation.tool === "string" ? invocation.tool : "unknown tool";
|
|
258
|
+
const outcome = mcpOutcome(payload);
|
|
259
|
+
const filePath = typeof input.file_path === "string" ? input.file_path : null;
|
|
260
|
+
|
|
261
|
+
if (filePath) {
|
|
262
|
+
return createSessionEvent({
|
|
263
|
+
applied: outcome.failed === null ? null : !outcome.failed,
|
|
264
|
+
atMs,
|
|
265
|
+
files: [filePath],
|
|
266
|
+
kind: SESSION_EVENT_KIND.EDIT,
|
|
267
|
+
sequence,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return createSessionEvent({
|
|
272
|
+
atMs,
|
|
273
|
+
command: typeof input.command === "string"
|
|
274
|
+
? input.command
|
|
275
|
+
: `mcp: ${server}/${tool}`,
|
|
276
|
+
error: outcome.error,
|
|
277
|
+
failed: outcome.failed,
|
|
278
|
+
kind: SESSION_EVENT_KIND.RAN,
|
|
279
|
+
sequence,
|
|
280
|
+
unclassified: false,
|
|
281
|
+
workdir: typeof input.workdir === "string" ? input.workdir : null,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function unmappedRecordType(recordValue) {
|
|
286
|
+
const outerType = typeof recordValue.type === "string" ? recordValue.type : "missing record type";
|
|
287
|
+
const payloadType = typeof recordValue.payload?.type === "string"
|
|
288
|
+
? recordValue.payload.type
|
|
289
|
+
: "missing payload type";
|
|
290
|
+
return `${outerType}:${payloadType}`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function outputText(payload) {
|
|
294
|
+
return contentText(payload.output ?? payload.content ?? payload.stderr ?? payload.stdout).trim() || null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function emptyResult({ cwd = null, origin = null, reason }) {
|
|
298
|
+
return createSessionEventsResult({
|
|
299
|
+
coverage: createSessionEventCoverage(),
|
|
300
|
+
events: [],
|
|
301
|
+
header: createSessionEventHeader({ cwd, origin, provider: PROVIDER_ID }),
|
|
302
|
+
reason,
|
|
303
|
+
window: {
|
|
304
|
+
complete: true,
|
|
305
|
+
end: null,
|
|
306
|
+
outcomesMayBeUnresolved: false,
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function findSessionRecord({ codexHome, id }) {
|
|
312
|
+
const record = await getSessionRecord({ codexHome, id });
|
|
313
|
+
if (record) return record;
|
|
314
|
+
return (await loadSessionStore({ codexHome })).recordsById.get(id) ?? null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export async function readSessionEvents({
|
|
318
|
+
codexHome,
|
|
319
|
+
id,
|
|
320
|
+
limit,
|
|
321
|
+
maxLineBytes,
|
|
322
|
+
mode,
|
|
323
|
+
signal,
|
|
324
|
+
}) {
|
|
325
|
+
const record = await findSessionRecord({ codexHome, id });
|
|
326
|
+
if (!record) return null;
|
|
327
|
+
|
|
328
|
+
const origin = record.recordSource ?? null;
|
|
329
|
+
if (!record.rolloutPath) {
|
|
330
|
+
return emptyResult({
|
|
331
|
+
cwd: record.cwd || null,
|
|
332
|
+
origin,
|
|
333
|
+
reason: SESSION_EVENT_REASON.NO_TRANSCRIPT_PATH,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const coverage = createSessionEventCoverage();
|
|
338
|
+
const readState = createSessionEventReadState({ limit, mode });
|
|
339
|
+
const unmappedTypes = createUnmappedSessionEventTracker();
|
|
340
|
+
const duplicateTexts = createDuplicateTextTracker();
|
|
341
|
+
let acceptingInjectedAsks = true;
|
|
342
|
+
let header = createSessionEventHeader({
|
|
343
|
+
cwd: record.cwd || null,
|
|
344
|
+
origin,
|
|
345
|
+
provider: PROVIDER_ID,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
function addEvent(event, pendingId = null) {
|
|
349
|
+
return readState.add(event, { pendingId });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function toolEvent(payload, atMs, sequence) {
|
|
353
|
+
const callId = payload.call_id ?? payload.id ?? null;
|
|
354
|
+
const name = typeof payload.name === "string" ? payload.name : "unknown tool";
|
|
355
|
+
|
|
356
|
+
if (name === "apply_patch") {
|
|
357
|
+
const details = patchDetails(payload.input);
|
|
358
|
+
return {
|
|
359
|
+
event: createSessionEvent({
|
|
360
|
+
...details,
|
|
361
|
+
atMs,
|
|
362
|
+
kind: SESSION_EVENT_KIND.EDIT,
|
|
363
|
+
sequence,
|
|
364
|
+
}),
|
|
365
|
+
pendingId: callId,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const argumentsValue = toolArguments(payload);
|
|
370
|
+
|
|
371
|
+
if (name === "request_user_input") {
|
|
372
|
+
const question = Array.isArray(argumentsValue?.questions)
|
|
373
|
+
? argumentsValue.questions
|
|
374
|
+
.map((item) => item?.question)
|
|
375
|
+
.filter((item) => typeof item === "string")
|
|
376
|
+
.join("\n")
|
|
377
|
+
: "";
|
|
378
|
+
return {
|
|
379
|
+
event: createSessionEvent({
|
|
380
|
+
atMs,
|
|
381
|
+
kind: SESSION_EVENT_KIND.DECIDED,
|
|
382
|
+
question: question || "Question",
|
|
383
|
+
sequence,
|
|
384
|
+
}),
|
|
385
|
+
pendingId: callId,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (name === "update_plan") {
|
|
390
|
+
const steps = Array.isArray(argumentsValue?.plan)
|
|
391
|
+
? argumentsValue.plan
|
|
392
|
+
.filter((step) => typeof step?.step === "string" && typeof step?.status === "string")
|
|
393
|
+
.map((step) => ({ status: step.status, text: step.step }))
|
|
394
|
+
: [];
|
|
395
|
+
return {
|
|
396
|
+
event: createSessionEvent({
|
|
397
|
+
atMs,
|
|
398
|
+
kind: SESSION_EVENT_KIND.PLAN,
|
|
399
|
+
sequence,
|
|
400
|
+
steps,
|
|
401
|
+
}),
|
|
402
|
+
pendingId: null,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const filePath = typeof argumentsValue?.file_path === "string"
|
|
407
|
+
? argumentsValue.file_path
|
|
408
|
+
: null;
|
|
409
|
+
if (filePath) {
|
|
410
|
+
return {
|
|
411
|
+
event: createSessionEvent({
|
|
412
|
+
atMs,
|
|
413
|
+
files: [filePath],
|
|
414
|
+
kind: SESSION_EVENT_KIND.EDIT,
|
|
415
|
+
sequence,
|
|
416
|
+
}),
|
|
417
|
+
pendingId: callId,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const command = typeof argumentsValue?.cmd === "string"
|
|
422
|
+
? argumentsValue.cmd
|
|
423
|
+
: typeof argumentsValue?.command === "string"
|
|
424
|
+
? argumentsValue.command
|
|
425
|
+
: null;
|
|
426
|
+
const execTool = name === "exec" || name === "exec_command";
|
|
427
|
+
const nestedLabel = execTool ? nestedToolLabel(nestedToolName(payload.input)) : null;
|
|
428
|
+
return {
|
|
429
|
+
event: createSessionEvent({
|
|
430
|
+
atMs,
|
|
431
|
+
command: command ?? nestedLabel ?? (execTool ? null : name),
|
|
432
|
+
kind: SESSION_EVENT_KIND.RAN,
|
|
433
|
+
sequence,
|
|
434
|
+
unclassified: command === null && (nestedLabel !== null || !execTool),
|
|
435
|
+
unextracted: execTool && command === null && nestedLabel === null,
|
|
436
|
+
workdir: typeof argumentsValue?.workdir === "string" ? argumentsValue.workdir : null,
|
|
437
|
+
}),
|
|
438
|
+
pendingId: callId,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function handleRecord(recordValue, sequence) {
|
|
443
|
+
const payload = recordValue.payload;
|
|
444
|
+
const atMs = asTimestamp(recordValue.timestamp ?? payload?.timestamp);
|
|
445
|
+
|
|
446
|
+
if (recordValue.type === "session_meta" && payload?.id) {
|
|
447
|
+
const git = payload.git && typeof payload.git === "object" ? {
|
|
448
|
+
branch: payload.git.branch ?? null,
|
|
449
|
+
commit: payload.git.commit ?? payload.git.commit_hash ?? null,
|
|
450
|
+
repository: payload.git.repository ?? payload.git.repository_url ?? null,
|
|
451
|
+
} : null;
|
|
452
|
+
header = createSessionEventHeader({
|
|
453
|
+
cwd: payload.cwd ?? header.cwd,
|
|
454
|
+
git,
|
|
455
|
+
model: payload.model ?? header.model,
|
|
456
|
+
origin: payload.originator ?? header.origin,
|
|
457
|
+
provider: PROVIDER_ID,
|
|
458
|
+
version: payload.cli_version ?? payload.version ?? null,
|
|
459
|
+
});
|
|
460
|
+
return { classification: RECORD_CLASSIFICATION.SKIPPED, stop: false };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (recordValue.type === "turn_context") {
|
|
464
|
+
header = createSessionEventHeader({
|
|
465
|
+
cwd: payload?.cwd ?? header.cwd,
|
|
466
|
+
git: header.git,
|
|
467
|
+
model: payload?.model ?? header.model,
|
|
468
|
+
origin: header.origin,
|
|
469
|
+
provider: PROVIDER_ID,
|
|
470
|
+
version: header.version,
|
|
471
|
+
});
|
|
472
|
+
return { classification: RECORD_CLASSIFICATION.SKIPPED, stop: false };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (payload?.type === "message") {
|
|
476
|
+
const text = contentText(payload.content);
|
|
477
|
+
if (!["assistant", "user"].includes(payload.role)) {
|
|
478
|
+
return { classification: RECORD_CLASSIFICATION.SKIPPED, stop: false };
|
|
479
|
+
}
|
|
480
|
+
if (!text) {
|
|
481
|
+
return { classification: RECORD_CLASSIFICATION.UNPARSEABLE, stop: false };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const injected = payload.role === "user"
|
|
485
|
+
&& acceptingInjectedAsks
|
|
486
|
+
&& isInjectedSessionAsk(text);
|
|
487
|
+
if (payload.role === "assistant" || !injected) acceptingInjectedAsks = false;
|
|
488
|
+
|
|
489
|
+
const kind = payload.role === "user" ? SESSION_EVENT_KIND.ASK : SESSION_EVENT_KIND.SAID;
|
|
490
|
+
if (duplicateTexts.isDuplicate(kind, text, sequence)) {
|
|
491
|
+
return { classification: RECORD_CLASSIFICATION.SKIPPED, duplicate: true, stop: false };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
classification: RECORD_CLASSIFICATION.RECOGNIZED,
|
|
496
|
+
stop: addEvent(createSessionEvent({ atMs, injected, kind, sequence, text })),
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (payload?.type === "thread_settings_applied") {
|
|
501
|
+
header = createSessionEventHeader({
|
|
502
|
+
cwd: header.cwd,
|
|
503
|
+
git: header.git,
|
|
504
|
+
model: payload.thread_settings?.model ?? header.model,
|
|
505
|
+
origin: header.origin,
|
|
506
|
+
provider: PROVIDER_ID,
|
|
507
|
+
version: header.version,
|
|
508
|
+
});
|
|
509
|
+
return { classification: RECORD_CLASSIFICATION.SKIPPED, stop: false };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (payload?.type === "web_search_end") {
|
|
513
|
+
const query = typeof payload.query === "string" ? payload.query : null;
|
|
514
|
+
return {
|
|
515
|
+
classification: RECORD_CLASSIFICATION.RECOGNIZED,
|
|
516
|
+
stop: addEvent(createSessionEvent({
|
|
517
|
+
atMs,
|
|
518
|
+
command: query ? `web search: ${query}` : "web search",
|
|
519
|
+
kind: SESSION_EVENT_KIND.RAN,
|
|
520
|
+
sequence,
|
|
521
|
+
})),
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (payload?.type === "task_complete" || payload?.type === "agent_message" || payload?.type === "user_message") {
|
|
526
|
+
const text = contentText(
|
|
527
|
+
payload.last_agent_message ?? payload.message ?? payload.text ?? payload.content,
|
|
528
|
+
);
|
|
529
|
+
if (!text) {
|
|
530
|
+
return { classification: RECORD_CLASSIFICATION.SKIPPED, stop: false };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const kind = payload.type === "user_message"
|
|
534
|
+
? SESSION_EVENT_KIND.ASK
|
|
535
|
+
: SESSION_EVENT_KIND.SAID;
|
|
536
|
+
if (duplicateTexts.isDuplicate(kind, text, sequence)) {
|
|
537
|
+
return { classification: RECORD_CLASSIFICATION.SKIPPED, duplicate: true, stop: false };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const injected = kind === SESSION_EVENT_KIND.ASK
|
|
541
|
+
&& acceptingInjectedAsks
|
|
542
|
+
&& isInjectedSessionAsk(text);
|
|
543
|
+
if (kind === SESSION_EVENT_KIND.SAID || !injected) acceptingInjectedAsks = false;
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
classification: RECORD_CLASSIFICATION.RECOGNIZED,
|
|
547
|
+
stop: addEvent(createSessionEvent({ atMs, injected, kind, sequence, text })),
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (recordValue.type === "compacted" || payload?.type === "compacted") {
|
|
552
|
+
acceptingInjectedAsks = false;
|
|
553
|
+
const text = contentText(
|
|
554
|
+
payload.content
|
|
555
|
+
?? payload.message
|
|
556
|
+
?? payload.summary
|
|
557
|
+
?? payload.replacement_history,
|
|
558
|
+
);
|
|
559
|
+
if (!text) {
|
|
560
|
+
return { classification: RECORD_CLASSIFICATION.UNPARSEABLE, stop: false };
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
classification: RECORD_CLASSIFICATION.RECOGNIZED,
|
|
564
|
+
stop: addEvent(createSessionEvent({
|
|
565
|
+
atMs,
|
|
566
|
+
kind: SESSION_EVENT_KIND.SUMMARY,
|
|
567
|
+
sequence,
|
|
568
|
+
text,
|
|
569
|
+
})),
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (payload?.type === "custom_tool_call" || payload?.type === "function_call") {
|
|
574
|
+
acceptingInjectedAsks = false;
|
|
575
|
+
const nestedName = ["exec", "exec_command"].includes(payload.name)
|
|
576
|
+
? nestedToolName(payload.input)
|
|
577
|
+
: null;
|
|
578
|
+
if (nestedName === "apply_patch" && toolArguments(payload).cmd === null) {
|
|
579
|
+
return { classification: RECORD_CLASSIFICATION.RECOGNIZED, stop: false };
|
|
580
|
+
}
|
|
581
|
+
const { event, pendingId } = toolEvent(payload, atMs, sequence);
|
|
582
|
+
return {
|
|
583
|
+
classification: RECORD_CLASSIFICATION.RECOGNIZED,
|
|
584
|
+
stop: addEvent(event, pendingId),
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (payload?.type === "patch_apply_begin") {
|
|
589
|
+
acceptingInjectedAsks = false;
|
|
590
|
+
const files = changePaths(payload.changes);
|
|
591
|
+
const resolved = readState.resolve(payload.call_id, (event) => {
|
|
592
|
+
if (event.kind === SESSION_EVENT_KIND.EDIT && files.length > 0) {
|
|
593
|
+
event.files = files;
|
|
594
|
+
}
|
|
595
|
+
}, { consume: false });
|
|
596
|
+
|
|
597
|
+
return {
|
|
598
|
+
classification: RECORD_CLASSIFICATION.RECOGNIZED,
|
|
599
|
+
stop: resolved
|
|
600
|
+
? false
|
|
601
|
+
: addEvent(createSessionEvent({
|
|
602
|
+
added: null,
|
|
603
|
+
applied: null,
|
|
604
|
+
atMs,
|
|
605
|
+
files,
|
|
606
|
+
kind: SESSION_EVENT_KIND.EDIT,
|
|
607
|
+
removed: null,
|
|
608
|
+
sequence,
|
|
609
|
+
}), payload.call_id),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (payload?.type === "patch_apply_end") {
|
|
614
|
+
acceptingInjectedAsks = false;
|
|
615
|
+
const details = patchResultDetails(payload);
|
|
616
|
+
const resolved = readState.resolve(payload.call_id, (event) => {
|
|
617
|
+
if (event.kind !== SESSION_EVENT_KIND.EDIT) return;
|
|
618
|
+
event.applied = details.applied;
|
|
619
|
+
if (details.files.length > 0) event.files = details.files;
|
|
620
|
+
});
|
|
621
|
+
return {
|
|
622
|
+
classification: RECORD_CLASSIFICATION.RECOGNIZED,
|
|
623
|
+
stop: resolved
|
|
624
|
+
? false
|
|
625
|
+
: addEvent(createSessionEvent({
|
|
626
|
+
...details,
|
|
627
|
+
atMs,
|
|
628
|
+
kind: SESSION_EVENT_KIND.EDIT,
|
|
629
|
+
sequence,
|
|
630
|
+
})),
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (payload?.type === "mcp_tool_call_end") {
|
|
635
|
+
acceptingInjectedAsks = false;
|
|
636
|
+
return {
|
|
637
|
+
classification: RECORD_CLASSIFICATION.RECOGNIZED,
|
|
638
|
+
stop: addEvent(mcpToolEvent(payload, atMs, sequence)),
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (
|
|
643
|
+
payload?.type === "custom_tool_call_output"
|
|
644
|
+
|| payload?.type === "function_call_output"
|
|
645
|
+
) {
|
|
646
|
+
const failed = commandFailure(payload);
|
|
647
|
+
const error = failed ? outputText(payload) : null;
|
|
648
|
+
readState.resolve(payload.call_id, (event) => {
|
|
649
|
+
if (event.kind === SESSION_EVENT_KIND.DECIDED) {
|
|
650
|
+
event.answer = outputText(payload);
|
|
651
|
+
} else if (event.kind === SESSION_EVENT_KIND.EDIT && failed !== null) {
|
|
652
|
+
event.applied = !failed;
|
|
653
|
+
} else if (event.kind === SESSION_EVENT_KIND.RAN && failed !== null) {
|
|
654
|
+
event.error = error;
|
|
655
|
+
event.failed = failed;
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
return { classification: RECORD_CLASSIFICATION.RECOGNIZED, stop: false };
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (
|
|
662
|
+
SKIPPED_RECORD_TYPES.has(recordValue.type)
|
|
663
|
+
|| SKIPPED_PAYLOAD_TYPES.has(payload?.type)
|
|
664
|
+
) {
|
|
665
|
+
return { classification: RECORD_CLASSIFICATION.SKIPPED, stop: false };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (typeof recordValue.type !== "string" && typeof payload?.type !== "string") {
|
|
669
|
+
return { classification: RECORD_CLASSIFICATION.UNPARSEABLE, stop: false };
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return {
|
|
673
|
+
classification: RECORD_CLASSIFICATION.UNMAPPED,
|
|
674
|
+
stop: false,
|
|
675
|
+
unmappedType: unmappedRecordType(recordValue),
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
let read;
|
|
680
|
+
|
|
681
|
+
try {
|
|
682
|
+
read = await visitJsonlSnapshotEntries(
|
|
683
|
+
record.rolloutPath,
|
|
684
|
+
(entry) => {
|
|
685
|
+
if (signal?.aborted) return false;
|
|
686
|
+
coverage.total += 1;
|
|
687
|
+
|
|
688
|
+
if (entry.oversized) {
|
|
689
|
+
coverage.oversized += 1;
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (!entry.parsed || typeof entry.parsed !== "object") {
|
|
694
|
+
coverage.unparseable += 1;
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const result = handleRecord(entry.parsed, entry.index);
|
|
699
|
+
coverage[result.classification] += 1;
|
|
700
|
+
if (result.duplicate) coverage.duplicates += 1;
|
|
701
|
+
if (result.classification === RECORD_CLASSIFICATION.UNMAPPED) {
|
|
702
|
+
unmappedTypes.add(result.unmappedType);
|
|
703
|
+
}
|
|
704
|
+
return !result.stop;
|
|
705
|
+
},
|
|
706
|
+
{ maxLineBytes },
|
|
707
|
+
);
|
|
708
|
+
} catch (error) {
|
|
709
|
+
if (error?.code === "ENOENT") {
|
|
710
|
+
return emptyResult({
|
|
711
|
+
cwd: record.cwd || null,
|
|
712
|
+
origin,
|
|
713
|
+
reason: SESSION_EVENT_REASON.TRANSCRIPT_MISSING,
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
throw error;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const events = readState.values();
|
|
721
|
+
coverage.unmappedTypes = unmappedTypes.values();
|
|
722
|
+
return createSessionEventsResult({
|
|
723
|
+
coverage,
|
|
724
|
+
events,
|
|
725
|
+
header,
|
|
726
|
+
reason: events.length === 0 && read.complete
|
|
727
|
+
? SESSION_EVENT_REASON.NO_RECOGNIZED_EVENTS
|
|
728
|
+
: null,
|
|
729
|
+
window: readState.window(read),
|
|
730
|
+
});
|
|
731
|
+
}
|