session-steward 0.4.0 → 0.5.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/CHANGELOG.md +31 -0
- package/README.md +13 -0
- package/bin/session-steward-cli.mjs +14 -0
- package/dist/assets/index-B8U6eLCA.css +2 -0
- package/dist/assets/index-JaDHeyls.js +9 -0
- package/dist/index.html +2 -2
- package/lib/cli.mjs +134 -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 +6 -1
- package/lib/providers/codex/database-families.mjs +2 -2
- package/lib/providers/codex/events.mjs +779 -0
- package/lib/providers/codex/index.mjs +2 -0
- package/lib/providers/codex/store.mjs +82 -4
- package/lib/server.mjs +62 -1
- 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 +6 -2
- package/dist/assets/index-BQ6SUqXr.css +0 -2
- package/dist/assets/index-QhQbSn0H.js +0 -9
package/dist/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
|
6
6
|
<link href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%23171717'/%3E%3Cpath d='M16 4 26 8v7c0 6-4 11-10 13C10 26 6 21 6 15V8Z' fill='%23f5f5f5'/%3E%3Cpath d='m9.5 16 2.2-2.2 3 3 6.5-6.5 2.2 2.2-8.7 8.7Z' fill='%23171717'/%3E%3C/svg%3E" rel="icon"/>
|
|
7
7
|
<title>Session Steward</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-JaDHeyls.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B8U6eLCA.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/lib/cli.mjs
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { once } from "node:events";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import readline from "node:readline/promises";
|
|
4
|
-
import process, {
|
|
4
|
+
import process, {
|
|
5
|
+
stderr as errorOutput,
|
|
6
|
+
stdin as input,
|
|
7
|
+
stdout as output,
|
|
8
|
+
} from "node:process";
|
|
5
9
|
|
|
6
10
|
import { getProvider } from "./providers/index.mjs";
|
|
11
|
+
import {
|
|
12
|
+
SESSION_EVENT_KIND,
|
|
13
|
+
SESSION_EVENT_REASON,
|
|
14
|
+
sessionEventCoveragePercent,
|
|
15
|
+
} from "./session-events.mjs";
|
|
7
16
|
|
|
8
17
|
function providerOptions(providerId, home) {
|
|
9
18
|
return providerId === "codex" ? { codexHome: home } : { claudeHome: home };
|
|
@@ -67,6 +76,8 @@ Options
|
|
|
67
76
|
--archive-status <status> Show all, active, or archived sessions
|
|
68
77
|
--sort <updated|created|name|cwd|size>
|
|
69
78
|
Choose the session order
|
|
79
|
+
--events Include the distilled session timeline
|
|
80
|
+
--events-limit <number> Limit timeline events (default 100)
|
|
70
81
|
--limit <number> Limit JSON results
|
|
71
82
|
-h, --help Show this help
|
|
72
83
|
`.trim();
|
|
@@ -331,6 +342,103 @@ function printInspect(record, deletionPlan) {
|
|
|
331
342
|
output.write(`Delete log rows: ${deletionPlan.logRowCount}\n`);
|
|
332
343
|
}
|
|
333
344
|
|
|
345
|
+
function eventTime(atMs) {
|
|
346
|
+
if (!Number.isFinite(atMs)) return "--:--";
|
|
347
|
+
return new Date(atMs).toLocaleTimeString([], {
|
|
348
|
+
hour: "2-digit",
|
|
349
|
+
hour12: false,
|
|
350
|
+
minute: "2-digit",
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function eventText(value) {
|
|
355
|
+
return typeof value === "string" ? value.replace(/\s+/gu, " ").trim() : "";
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function planSummary(steps) {
|
|
359
|
+
const counts = new Map();
|
|
360
|
+
for (const step of steps) {
|
|
361
|
+
const status = step.status.replaceAll("_", " ");
|
|
362
|
+
counts.set(status, (counts.get(status) ?? 0) + 1);
|
|
363
|
+
}
|
|
364
|
+
return [...counts]
|
|
365
|
+
.map(([status, count]) => `${count} ${status}`)
|
|
366
|
+
.join(" · ") || "No steps recorded";
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function sessionEventDescription(event) {
|
|
370
|
+
if (event.kind === SESSION_EVENT_KIND.ASK) {
|
|
371
|
+
return `${event.injected ? "[injected context] " : ""}${eventText(event.text)}`;
|
|
372
|
+
}
|
|
373
|
+
if (event.kind === SESSION_EVENT_KIND.SAID || event.kind === SESSION_EVENT_KIND.SUMMARY) {
|
|
374
|
+
return eventText(event.text);
|
|
375
|
+
}
|
|
376
|
+
if (event.kind === SESSION_EVENT_KIND.EDIT) {
|
|
377
|
+
const files = event.files.length > 0 ? event.files.join(", ") : "File not recorded";
|
|
378
|
+
const changes = Number.isInteger(event.added) && Number.isInteger(event.removed)
|
|
379
|
+
? ` +${event.added}/-${event.removed}`
|
|
380
|
+
: "";
|
|
381
|
+
const outcome = event.applied === true
|
|
382
|
+
? " applied"
|
|
383
|
+
: event.applied === false
|
|
384
|
+
? " NOT APPLIED"
|
|
385
|
+
: "";
|
|
386
|
+
return `${files}${changes}${outcome}`;
|
|
387
|
+
}
|
|
388
|
+
if (event.kind === SESSION_EVENT_KIND.RAN) {
|
|
389
|
+
const command = eventText(event.command) || "Command not recorded";
|
|
390
|
+
const outcome = event.failed === true ? " FAILED" : "";
|
|
391
|
+
const error = event.failed === true && event.error ? ` ${eventText(event.error)}` : "";
|
|
392
|
+
return `${command}${outcome}${error}`;
|
|
393
|
+
}
|
|
394
|
+
if (event.kind === SESSION_EVENT_KIND.DECIDED) {
|
|
395
|
+
const answer = event.answer ? ` → ${eventText(event.answer)}` : "";
|
|
396
|
+
return `${eventText(event.question)}${answer}`;
|
|
397
|
+
}
|
|
398
|
+
if (event.kind === SESSION_EVENT_KIND.PLAN) return planSummary(event.steps);
|
|
399
|
+
return "Event details unavailable";
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function sessionEventReason(reason) {
|
|
403
|
+
return {
|
|
404
|
+
[SESSION_EVENT_REASON.NO_RECOGNIZED_EVENTS]: "No recognized session events were found.",
|
|
405
|
+
[SESSION_EVENT_REASON.NO_TRANSCRIPT_PATH]: "No transcript path was recorded for this session.",
|
|
406
|
+
[SESSION_EVENT_REASON.TRANSCRIPT_MISSING]: "The transcript file is missing.",
|
|
407
|
+
}[reason] ?? null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function printSessionEventCoverage(coverage) {
|
|
411
|
+
const details = [
|
|
412
|
+
`recognized ${sessionEventCoveragePercent(coverage)}%`,
|
|
413
|
+
`${coverage.skipped} skipped`,
|
|
414
|
+
];
|
|
415
|
+
if (coverage.unmapped > 0) details.push(`${coverage.unmapped} unmapped`);
|
|
416
|
+
if (coverage.unparseable > 0) details.push(`${coverage.unparseable} unparseable`);
|
|
417
|
+
if (coverage.oversized > 0) details.push(`${coverage.oversized} oversized`);
|
|
418
|
+
errorOutput.write(`${details.join(" · ")}\n`);
|
|
419
|
+
if (coverage.unmappedTypes.length > 0) {
|
|
420
|
+
errorOutput.write(`unmapped types: ${coverage.unmappedTypes.map(({ count, type }) => `${type} (${count})`).join(", ")}\n`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function printSessionEvents(result) {
|
|
425
|
+
output.write("\nSession timeline\n");
|
|
426
|
+
output.write("----------------\n");
|
|
427
|
+
const reason = sessionEventReason(result.reason);
|
|
428
|
+
|
|
429
|
+
if (reason) {
|
|
430
|
+
output.write(`${reason}\n`);
|
|
431
|
+
} else {
|
|
432
|
+
const width = output.isTTY && Number.isInteger(output.columns) ? output.columns : 120;
|
|
433
|
+
for (const event of [...result.events].reverse()) {
|
|
434
|
+
const kind = event.kind.toUpperCase().padEnd(8, " ");
|
|
435
|
+
output.write(`${truncate(`${eventTime(event.atMs)} ${kind} ${sessionEventDescription(event)}`, width)}\n`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
printSessionEventCoverage(result.coverage);
|
|
440
|
+
}
|
|
441
|
+
|
|
334
442
|
function printDeletionPreview(plan, preflight, scope) {
|
|
335
443
|
const fileCount = preflight.transcriptFileCount ?? plan.transcriptFileCount;
|
|
336
444
|
const sessionBytes = preflight.transcriptBytes ?? plan.transcriptBytes;
|
|
@@ -537,7 +645,21 @@ async function printJson(options) {
|
|
|
537
645
|
|
|
538
646
|
for (const record of result.records) {
|
|
539
647
|
if (written >= limit) break;
|
|
540
|
-
|
|
648
|
+
let serializedRecord = options.provider.formatSessionForJson(record);
|
|
649
|
+
if (options.events) {
|
|
650
|
+
const eventResult = await options.provider.readSessionEvents({
|
|
651
|
+
...providerOptions(options.provider.id, options.providerHome),
|
|
652
|
+
id: record.id,
|
|
653
|
+
limit: options.eventsLimit,
|
|
654
|
+
});
|
|
655
|
+
serializedRecord = {
|
|
656
|
+
...serializedRecord,
|
|
657
|
+
coverage: eventResult.coverage,
|
|
658
|
+
events: eventResult.events,
|
|
659
|
+
header: eventResult.header,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
await writeChunk(`${first ? "" : ",\n"}${JSON.stringify(serializedRecord, null, 2)}`);
|
|
541
663
|
first = false;
|
|
542
664
|
written += 1;
|
|
543
665
|
}
|
|
@@ -797,6 +919,14 @@ async function runInteractive(state) {
|
|
|
797
919
|
});
|
|
798
920
|
|
|
799
921
|
printInspect(record, deletionPlan);
|
|
922
|
+
if (state.showEvents) {
|
|
923
|
+
const eventResult = await state.provider.readSessionEvents({
|
|
924
|
+
...providerOptions(state.provider.id, state.providerHome),
|
|
925
|
+
id: record.id,
|
|
926
|
+
limit: state.eventsLimit,
|
|
927
|
+
});
|
|
928
|
+
printSessionEvents(eventResult);
|
|
929
|
+
}
|
|
800
930
|
} catch (error) {
|
|
801
931
|
output.write(`\n${error instanceof Error ? error.message : String(error)}\n`);
|
|
802
932
|
}
|
|
@@ -970,6 +1100,7 @@ export async function runCli(options) {
|
|
|
970
1100
|
const state = {
|
|
971
1101
|
archiveStatus: validateArchiveStatus(options.archiveStatus),
|
|
972
1102
|
cleanupMode: validateCleanupMode(options.cleanup),
|
|
1103
|
+
eventsLimit: options.eventsLimit,
|
|
973
1104
|
provider,
|
|
974
1105
|
providerHome,
|
|
975
1106
|
inactiveDays: validateInactiveDays(options.inactiveDays),
|
|
@@ -978,6 +1109,7 @@ export async function runCli(options) {
|
|
|
978
1109
|
result: null,
|
|
979
1110
|
search: options.search || "",
|
|
980
1111
|
showInternals: Boolean(options.includeInternals),
|
|
1112
|
+
showEvents: Boolean(options.events),
|
|
981
1113
|
showSupporting: Boolean(options.includeSupporting),
|
|
982
1114
|
sort: validateSort(options.sort || "updated"),
|
|
983
1115
|
workspace: options.workspace,
|
|
@@ -0,0 +1,498 @@
|
|
|
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 } from "./store.mjs";
|
|
16
|
+
|
|
17
|
+
const MAX_PENDING_DECISIONS = 2_048;
|
|
18
|
+
const PROVIDER_ID = "claude-code";
|
|
19
|
+
const READ_ONLY_TOOLS = new Set(["Glob", "Grep", "Read", "WebFetch", "WebSearch"]);
|
|
20
|
+
const RECORD_CLASSIFICATION = Object.freeze({
|
|
21
|
+
RECOGNIZED: "recognized",
|
|
22
|
+
SKIPPED: "skipped",
|
|
23
|
+
UNMAPPED: "unmapped",
|
|
24
|
+
UNPARSEABLE: "unparseable",
|
|
25
|
+
});
|
|
26
|
+
const SKIPPED_CONTENT_TYPES = new Set(["attachment", "image", "thinking"]);
|
|
27
|
+
const SKIPPED_RECORD_TYPES = new Set([
|
|
28
|
+
"attachment",
|
|
29
|
+
"custom-title",
|
|
30
|
+
"file-history-snapshot",
|
|
31
|
+
"frame-link",
|
|
32
|
+
"last-prompt",
|
|
33
|
+
"mode",
|
|
34
|
+
"queue-operation",
|
|
35
|
+
"summary",
|
|
36
|
+
"system",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
function asTimestamp(value) {
|
|
40
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
41
|
+
if (typeof value !== "string") return null;
|
|
42
|
+
const timestamp = Date.parse(value);
|
|
43
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function contentText(value) {
|
|
47
|
+
if (typeof value === "string") return value;
|
|
48
|
+
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
return value
|
|
51
|
+
.map((part) => contentText(part))
|
|
52
|
+
.filter((part) => part !== "")
|
|
53
|
+
.join("\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!value || typeof value !== "object") return "";
|
|
57
|
+
if (typeof value.text === "string") return value.text;
|
|
58
|
+
if (typeof value.content === "string" || Array.isArray(value.content)) {
|
|
59
|
+
return contentText(value.content);
|
|
60
|
+
}
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function emptyResult({ cwd = null, origin = null, reason }) {
|
|
65
|
+
return createSessionEventsResult({
|
|
66
|
+
coverage: createSessionEventCoverage(),
|
|
67
|
+
events: [],
|
|
68
|
+
header: createSessionEventHeader({ cwd, origin, provider: PROVIDER_ID }),
|
|
69
|
+
reason,
|
|
70
|
+
window: {
|
|
71
|
+
complete: true,
|
|
72
|
+
end: null,
|
|
73
|
+
outcomesMayBeUnresolved: false,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function messageParts(record) {
|
|
79
|
+
const content = record.message?.content;
|
|
80
|
+
if (typeof content === "string") return [{ text: content, type: "text" }];
|
|
81
|
+
return Array.isArray(content) ? content : [];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function planSteps(input) {
|
|
85
|
+
const todos = Array.isArray(input?.todos) ? input.todos : [];
|
|
86
|
+
return todos
|
|
87
|
+
.filter((todo) => typeof todo?.status === "string")
|
|
88
|
+
.map((todo) => ({
|
|
89
|
+
status: todo.status,
|
|
90
|
+
text: typeof todo.content === "string"
|
|
91
|
+
? todo.content
|
|
92
|
+
: typeof todo.activeForm === "string"
|
|
93
|
+
? todo.activeForm
|
|
94
|
+
: "Untitled task",
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function questionText(input) {
|
|
99
|
+
if (typeof input?.question === "string") return input.question;
|
|
100
|
+
if (!Array.isArray(input?.questions)) return "Question";
|
|
101
|
+
return input.questions
|
|
102
|
+
.map((question) => question?.question)
|
|
103
|
+
.filter((question) => typeof question === "string")
|
|
104
|
+
.join("\n") || "Question";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function readSessionEvents({
|
|
108
|
+
claudeHome,
|
|
109
|
+
desktopDataHome,
|
|
110
|
+
id,
|
|
111
|
+
limit,
|
|
112
|
+
maxLineBytes,
|
|
113
|
+
mode,
|
|
114
|
+
signal,
|
|
115
|
+
}) {
|
|
116
|
+
const record = await getSessionRecord({ claudeHome, desktopDataHome, id });
|
|
117
|
+
if (!record) return null;
|
|
118
|
+
|
|
119
|
+
const origin = record.surface ?? record.recordSource ?? null;
|
|
120
|
+
if (!record.rolloutPath) {
|
|
121
|
+
return emptyResult({
|
|
122
|
+
cwd: record.cwd || null,
|
|
123
|
+
origin,
|
|
124
|
+
reason: SESSION_EVENT_REASON.NO_TRANSCRIPT_PATH,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const coverage = createSessionEventCoverage();
|
|
129
|
+
const pendingDecisionIds = [];
|
|
130
|
+
const readState = createSessionEventReadState({ limit, mode });
|
|
131
|
+
const unmappedTypes = createUnmappedSessionEventTracker();
|
|
132
|
+
let acceptingInjectedAsks = true;
|
|
133
|
+
let header = createSessionEventHeader({
|
|
134
|
+
cwd: record.cwd || null,
|
|
135
|
+
origin,
|
|
136
|
+
provider: PROVIDER_ID,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
function addEvent(event, pendingId = null) {
|
|
140
|
+
return readState.add(event, { pendingId });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function rememberDecision(pendingId) {
|
|
144
|
+
if (!pendingId) return;
|
|
145
|
+
pendingDecisionIds.push(pendingId);
|
|
146
|
+
if (pendingDecisionIds.length > MAX_PENDING_DECISIONS) pendingDecisionIds.shift();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function resolveDecision(answer) {
|
|
150
|
+
while (pendingDecisionIds.length > 0) {
|
|
151
|
+
const pendingId = pendingDecisionIds.shift();
|
|
152
|
+
if (readState.resolve(pendingId, (event) => {
|
|
153
|
+
if (event.kind === SESSION_EVENT_KIND.DECIDED) event.answer = answer;
|
|
154
|
+
})) return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function toolEvent(part, atMs, sequence) {
|
|
161
|
+
const input = part.input && typeof part.input === "object" ? part.input : {};
|
|
162
|
+
const name = typeof part.name === "string" ? part.name : "unknown tool";
|
|
163
|
+
const pendingId = typeof part.id === "string" ? part.id : null;
|
|
164
|
+
|
|
165
|
+
if (name === "Edit" || name === "Write") {
|
|
166
|
+
return {
|
|
167
|
+
event: createSessionEvent({
|
|
168
|
+
atMs,
|
|
169
|
+
files: typeof input.file_path === "string" ? [input.file_path] : [],
|
|
170
|
+
kind: SESSION_EVENT_KIND.EDIT,
|
|
171
|
+
sequence,
|
|
172
|
+
}),
|
|
173
|
+
pendingId,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (name === "Bash") {
|
|
178
|
+
return {
|
|
179
|
+
event: createSessionEvent({
|
|
180
|
+
atMs,
|
|
181
|
+
command: typeof input.command === "string" ? input.command : null,
|
|
182
|
+
kind: SESSION_EVENT_KIND.RAN,
|
|
183
|
+
sequence,
|
|
184
|
+
workdir: typeof input.workdir === "string" ? input.workdir : record.cwd || null,
|
|
185
|
+
}),
|
|
186
|
+
pendingId,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (name === "AskUserQuestion") {
|
|
191
|
+
return {
|
|
192
|
+
decision: true,
|
|
193
|
+
event: createSessionEvent({
|
|
194
|
+
atMs,
|
|
195
|
+
kind: SESSION_EVENT_KIND.DECIDED,
|
|
196
|
+
question: questionText(input),
|
|
197
|
+
sequence,
|
|
198
|
+
}),
|
|
199
|
+
pendingId,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (name === "TodoWrite") {
|
|
204
|
+
return {
|
|
205
|
+
event: createSessionEvent({
|
|
206
|
+
atMs,
|
|
207
|
+
kind: SESSION_EVENT_KIND.PLAN,
|
|
208
|
+
sequence,
|
|
209
|
+
steps: planSteps(input),
|
|
210
|
+
}),
|
|
211
|
+
pendingId: null,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!READ_ONLY_TOOLS.has(name) && typeof input.file_path === "string") {
|
|
216
|
+
return {
|
|
217
|
+
event: createSessionEvent({
|
|
218
|
+
atMs,
|
|
219
|
+
files: [input.file_path],
|
|
220
|
+
kind: SESSION_EVENT_KIND.EDIT,
|
|
221
|
+
sequence,
|
|
222
|
+
}),
|
|
223
|
+
pendingId,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (typeof input.command === "string") {
|
|
228
|
+
return {
|
|
229
|
+
event: createSessionEvent({
|
|
230
|
+
atMs,
|
|
231
|
+
command: input.command,
|
|
232
|
+
kind: SESSION_EVENT_KIND.RAN,
|
|
233
|
+
sequence,
|
|
234
|
+
workdir: typeof input.workdir === "string" ? input.workdir : record.cwd || null,
|
|
235
|
+
}),
|
|
236
|
+
pendingId,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
event: createSessionEvent({
|
|
242
|
+
atMs,
|
|
243
|
+
command: name,
|
|
244
|
+
kind: SESSION_EVENT_KIND.RAN,
|
|
245
|
+
sequence,
|
|
246
|
+
unclassified: true,
|
|
247
|
+
workdir: record.cwd || null,
|
|
248
|
+
}),
|
|
249
|
+
pendingId,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function resolveToolResult(part) {
|
|
254
|
+
const pendingId = part.tool_use_id;
|
|
255
|
+
const answer = contentText(part.content).trim();
|
|
256
|
+
const failed = part.is_error === true;
|
|
257
|
+
return readState.resolve(pendingId, (event) => {
|
|
258
|
+
if (event.kind === SESSION_EVENT_KIND.DECIDED) {
|
|
259
|
+
event.answer = answer || null;
|
|
260
|
+
} else if (event.kind === SESSION_EVENT_KIND.EDIT) {
|
|
261
|
+
event.applied = !failed;
|
|
262
|
+
} else if (event.kind === SESSION_EVENT_KIND.RAN) {
|
|
263
|
+
event.error = failed ? answer || "Tool failed." : null;
|
|
264
|
+
event.failed = failed;
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function updateHeader(recordValue) {
|
|
270
|
+
const branch = recordValue.gitBranch ?? recordValue.git?.branch ?? header.git?.branch ?? null;
|
|
271
|
+
const commit = recordValue.git?.commit ?? recordValue.git?.commit_hash ?? header.git?.commit ?? null;
|
|
272
|
+
const repository = recordValue.git?.repository
|
|
273
|
+
?? recordValue.git?.repository_url
|
|
274
|
+
?? header.git?.repository
|
|
275
|
+
?? null;
|
|
276
|
+
const hasGit = branch !== null || commit !== null || repository !== null;
|
|
277
|
+
header = createSessionEventHeader({
|
|
278
|
+
cwd: recordValue.cwd ?? header.cwd,
|
|
279
|
+
git: hasGit ? { branch, commit, repository } : null,
|
|
280
|
+
model: recordValue.message?.model ?? recordValue.model ?? header.model,
|
|
281
|
+
origin: recordValue.entrypoint ?? header.origin,
|
|
282
|
+
provider: PROVIDER_ID,
|
|
283
|
+
version: recordValue.version ?? header.version,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function handleRecord(recordValue, sequence) {
|
|
288
|
+
updateHeader(recordValue);
|
|
289
|
+
const atMs = asTimestamp(recordValue.timestamp ?? recordValue.createdAt);
|
|
290
|
+
const parts = messageParts(recordValue);
|
|
291
|
+
|
|
292
|
+
if (recordValue.type === "user") {
|
|
293
|
+
if (recordValue.isCompactSummary === true) {
|
|
294
|
+
acceptingInjectedAsks = false;
|
|
295
|
+
const text = contentText(recordValue.message?.content);
|
|
296
|
+
if (!text) {
|
|
297
|
+
return { classification: RECORD_CLASSIFICATION.UNPARSEABLE, stop: false };
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
classification: RECORD_CLASSIFICATION.RECOGNIZED,
|
|
301
|
+
stop: addEvent(createSessionEvent({
|
|
302
|
+
atMs,
|
|
303
|
+
kind: SESSION_EVENT_KIND.SUMMARY,
|
|
304
|
+
sequence,
|
|
305
|
+
text,
|
|
306
|
+
})),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let recognized = false;
|
|
311
|
+
let resolvedDecision = false;
|
|
312
|
+
let unparseable = parts.length === 0;
|
|
313
|
+
let unmappedType = null;
|
|
314
|
+
|
|
315
|
+
for (const part of parts) {
|
|
316
|
+
if (typeof part?.type !== "string") {
|
|
317
|
+
unparseable = true;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (part?.type === "text" && typeof part.text === "string") continue;
|
|
321
|
+
if (part?.type !== "tool_result") {
|
|
322
|
+
if (!SKIPPED_CONTENT_TYPES.has(part?.type)) {
|
|
323
|
+
unmappedType = `user:${part?.type ?? "missing content type"}`;
|
|
324
|
+
}
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const resolved = resolveToolResult(part);
|
|
328
|
+
if (part.is_error === true) recognized = true;
|
|
329
|
+
if (part.is_error === true && !resolved) unparseable = true;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const text = parts
|
|
333
|
+
.filter((part) => part?.type === "text" && typeof part.text === "string")
|
|
334
|
+
.map((part) => part.text)
|
|
335
|
+
.join("\n");
|
|
336
|
+
|
|
337
|
+
if (text) {
|
|
338
|
+
resolvedDecision = resolveDecision(text);
|
|
339
|
+
recognized = true;
|
|
340
|
+
if (resolvedDecision) acceptingInjectedAsks = false;
|
|
341
|
+
if (!resolvedDecision) {
|
|
342
|
+
const injected = acceptingInjectedAsks && isInjectedSessionAsk(text);
|
|
343
|
+
if (!injected) acceptingInjectedAsks = false;
|
|
344
|
+
return {
|
|
345
|
+
classification: unmappedType
|
|
346
|
+
? RECORD_CLASSIFICATION.UNMAPPED
|
|
347
|
+
: unparseable
|
|
348
|
+
? RECORD_CLASSIFICATION.UNPARSEABLE
|
|
349
|
+
: RECORD_CLASSIFICATION.RECOGNIZED,
|
|
350
|
+
stop: addEvent(createSessionEvent({
|
|
351
|
+
atMs,
|
|
352
|
+
injected,
|
|
353
|
+
kind: SESSION_EVENT_KIND.ASK,
|
|
354
|
+
sequence,
|
|
355
|
+
text,
|
|
356
|
+
})),
|
|
357
|
+
unmappedType,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
classification: unmappedType
|
|
364
|
+
? RECORD_CLASSIFICATION.UNMAPPED
|
|
365
|
+
: unparseable
|
|
366
|
+
? RECORD_CLASSIFICATION.UNPARSEABLE
|
|
367
|
+
: recognized
|
|
368
|
+
? RECORD_CLASSIFICATION.RECOGNIZED
|
|
369
|
+
: RECORD_CLASSIFICATION.SKIPPED,
|
|
370
|
+
stop: false,
|
|
371
|
+
unmappedType,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (recordValue.type === "assistant") {
|
|
376
|
+
acceptingInjectedAsks = false;
|
|
377
|
+
let recognized = false;
|
|
378
|
+
let stop = false;
|
|
379
|
+
const unparseable = parts.length === 0 || parts.some((part) => (
|
|
380
|
+
typeof part?.type !== "string"
|
|
381
|
+
|| (part.type === "text" && typeof part.text !== "string")
|
|
382
|
+
));
|
|
383
|
+
const unmappedPart = parts.find((part) => (
|
|
384
|
+
typeof part?.type === "string"
|
|
385
|
+
&& part.type !== "text"
|
|
386
|
+
&& part?.type !== "tool_use"
|
|
387
|
+
&& !SKIPPED_CONTENT_TYPES.has(part?.type)
|
|
388
|
+
));
|
|
389
|
+
const unmappedType = unmappedPart
|
|
390
|
+
? `assistant:${unmappedPart?.type ?? "missing content type"}`
|
|
391
|
+
: null;
|
|
392
|
+
const text = parts
|
|
393
|
+
.filter((part) => part?.type === "text" && typeof part.text === "string")
|
|
394
|
+
.map((part) => part.text)
|
|
395
|
+
.join("\n");
|
|
396
|
+
|
|
397
|
+
if (text) {
|
|
398
|
+
recognized = true;
|
|
399
|
+
stop = addEvent(createSessionEvent({
|
|
400
|
+
atMs,
|
|
401
|
+
kind: SESSION_EVENT_KIND.SAID,
|
|
402
|
+
sequence,
|
|
403
|
+
text,
|
|
404
|
+
}));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (!stop) {
|
|
408
|
+
for (const part of parts) {
|
|
409
|
+
if (part?.type === "text" && typeof part.text === "string") continue;
|
|
410
|
+
if (part?.type !== "tool_use") {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const result = toolEvent(part, atMs, sequence);
|
|
414
|
+
recognized = true;
|
|
415
|
+
stop = addEvent(result.event, result.pendingId);
|
|
416
|
+
if (result.decision) rememberDecision(result.pendingId);
|
|
417
|
+
if (stop) break;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
classification: unmappedType
|
|
423
|
+
? RECORD_CLASSIFICATION.UNMAPPED
|
|
424
|
+
: unparseable
|
|
425
|
+
? RECORD_CLASSIFICATION.UNPARSEABLE
|
|
426
|
+
: recognized
|
|
427
|
+
? RECORD_CLASSIFICATION.RECOGNIZED
|
|
428
|
+
: RECORD_CLASSIFICATION.SKIPPED,
|
|
429
|
+
stop,
|
|
430
|
+
unmappedType,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (typeof recordValue.type !== "string") {
|
|
435
|
+
return { classification: RECORD_CLASSIFICATION.UNPARSEABLE, stop: false };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
classification: SKIPPED_RECORD_TYPES.has(recordValue.type)
|
|
440
|
+
? RECORD_CLASSIFICATION.SKIPPED
|
|
441
|
+
: RECORD_CLASSIFICATION.UNMAPPED,
|
|
442
|
+
stop: false,
|
|
443
|
+
unmappedType: `claude:${recordValue.type ?? "missing record type"}`,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
let read;
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
read = await visitJsonlSnapshotEntries(
|
|
451
|
+
record.rolloutPath,
|
|
452
|
+
(entry) => {
|
|
453
|
+
if (signal?.aborted) return false;
|
|
454
|
+
coverage.total += 1;
|
|
455
|
+
|
|
456
|
+
if (entry.oversized) {
|
|
457
|
+
coverage.oversized += 1;
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (!entry.parsed || typeof entry.parsed !== "object") {
|
|
462
|
+
coverage.unparseable += 1;
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const result = handleRecord(entry.parsed, entry.index);
|
|
467
|
+
coverage[result.classification] += 1;
|
|
468
|
+
if (result.classification === RECORD_CLASSIFICATION.UNMAPPED) {
|
|
469
|
+
unmappedTypes.add(result.unmappedType);
|
|
470
|
+
}
|
|
471
|
+
return !result.stop;
|
|
472
|
+
},
|
|
473
|
+
{ maxLineBytes },
|
|
474
|
+
);
|
|
475
|
+
} catch (error) {
|
|
476
|
+
if (error?.code === "ENOENT") {
|
|
477
|
+
return emptyResult({
|
|
478
|
+
cwd: record.cwd || null,
|
|
479
|
+
origin,
|
|
480
|
+
reason: SESSION_EVENT_REASON.TRANSCRIPT_MISSING,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
throw error;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const events = readState.values();
|
|
488
|
+
coverage.unmappedTypes = unmappedTypes.values();
|
|
489
|
+
return createSessionEventsResult({
|
|
490
|
+
coverage,
|
|
491
|
+
events,
|
|
492
|
+
header,
|
|
493
|
+
reason: events.length === 0 && read.complete
|
|
494
|
+
? SESSION_EVENT_REASON.NO_RECOGNIZED_EVENTS
|
|
495
|
+
: null,
|
|
496
|
+
window: readState.window(read),
|
|
497
|
+
});
|
|
498
|
+
}
|