@termfleet/core 0.1.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/dist/agent-launch.d.ts +78 -0
- package/dist/agent-launch.js +247 -0
- package/dist/agent-session-id.d.ts +10 -0
- package/dist/agent-session-id.js +36 -0
- package/dist/agent-session-index-client.d.ts +7 -0
- package/dist/agent-session-index-client.js +86 -0
- package/dist/agent-session-index-worker.d.ts +1 -0
- package/dist/agent-session-index-worker.js +20 -0
- package/dist/agent-session-index.d.ts +34 -0
- package/dist/agent-session-index.js +527 -0
- package/dist/agent-session-tail.d.ts +33 -0
- package/dist/agent-session-tail.js +184 -0
- package/dist/agent-session-watcher.d.ts +36 -0
- package/dist/agent-session-watcher.js +194 -0
- package/dist/agent-session.d.ts +380 -0
- package/dist/agent-session.js +1688 -0
- package/dist/background-runner.d.ts +3 -0
- package/dist/background-runner.js +55 -0
- package/dist/boot-queue.d.ts +35 -0
- package/dist/boot-queue.js +66 -0
- package/dist/build-info.d.ts +5 -0
- package/dist/build-info.js +38 -0
- package/dist/collab/canvas-doc.d.ts +47 -0
- package/dist/collab/canvas-doc.js +83 -0
- package/dist/contracts/auth.d.ts +77 -0
- package/dist/contracts/auth.js +1 -0
- package/dist/contracts/canvas.d.ts +34 -0
- package/dist/contracts/canvas.js +76 -0
- package/dist/contracts/console-layout.d.ts +39 -0
- package/dist/contracts/console-layout.js +135 -0
- package/dist/contracts/files.d.ts +38 -0
- package/dist/contracts/files.js +37 -0
- package/dist/contracts/provider-url.d.ts +3 -0
- package/dist/contracts/provider-url.js +49 -0
- package/dist/contracts/registry.d.ts +58 -0
- package/dist/contracts/registry.js +285 -0
- package/dist/launch-trace.d.ts +6 -0
- package/dist/launch-trace.js +33 -0
- package/dist/lib/errors.d.ts +1 -0
- package/dist/lib/errors.js +5 -0
- package/dist/lib/exec.d.ts +13 -0
- package/dist/lib/exec.js +134 -0
- package/dist/local-providers.d.ts +32 -0
- package/dist/local-providers.js +184 -0
- package/dist/local-tunnel.d.ts +6 -0
- package/dist/local-tunnel.js +258 -0
- package/dist/provider-access-token.d.ts +11 -0
- package/dist/provider-access-token.js +77 -0
- package/dist/provider-client.d.ts +152 -0
- package/dist/provider-client.js +666 -0
- package/dist/provider-url-resolver.d.ts +16 -0
- package/dist/provider-url-resolver.js +37 -0
- package/dist/registry-client.d.ts +93 -0
- package/dist/registry-client.js +170 -0
- package/dist/registry.d.ts +56 -0
- package/dist/registry.js +406 -0
- package/dist/session-attention.d.ts +24 -0
- package/dist/session-attention.js +54 -0
- package/dist/session-lifecycle.d.ts +83 -0
- package/dist/session-lifecycle.js +658 -0
- package/dist/session-window.d.ts +3 -0
- package/dist/session-window.js +20 -0
- package/dist/terminal-client.d.ts +49 -0
- package/dist/terminal-client.js +89 -0
- package/dist/types.d.ts +155 -0
- package/dist/types.js +21 -0
- package/package.json +26 -0
|
@@ -0,0 +1,1688 @@
|
|
|
1
|
+
import { closeSync, existsSync, openSync, readdirSync, readFileSync, readSync, statSync } from "node:fs";
|
|
2
|
+
import { readFile as readFileAsync } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
|
+
// Agent transcripts live under the host home by default. TERMFLEET_AGENT_HOME
|
|
6
|
+
// overrides the root so throwaway stacks and tests can seed a deterministic
|
|
7
|
+
// `.claude`/`.codex` tree without touching the real home directory.
|
|
8
|
+
export function agentHome() {
|
|
9
|
+
return process.env.TERMFLEET_AGENT_HOME || homedir();
|
|
10
|
+
}
|
|
11
|
+
// A bare session id becomes a path segment (the transcript file name), so before
|
|
12
|
+
// it is used to BUILD A PATH it must be constrained to a filename-safe alphabet —
|
|
13
|
+
// otherwise a value like "../../../../etc/passwd" climbs out of the agent-home
|
|
14
|
+
// tree and turns a transcript read into an arbitrary-file read. Real
|
|
15
|
+
// Claude/Codex/Gemini ids are UUIDs (legacy ids are numeric pids), so this never
|
|
16
|
+
// rejects a genuine id. Applied at each transcript-path sink, NOT in the
|
|
17
|
+
// normalize* strippers — those are also used to derive id fields (e.g. the
|
|
18
|
+
// composite "claude-subagent:<parent>:<agent>" id), which legitimately contain
|
|
19
|
+
// characters outside the path alphabet.
|
|
20
|
+
export function assertSafeAgentSessionId(bareId) {
|
|
21
|
+
if (!/^[A-Za-z0-9_-]+$/.test(bareId)) {
|
|
22
|
+
throw new Error(`Invalid agent session id: ${JSON.stringify(bareId)}`);
|
|
23
|
+
}
|
|
24
|
+
return bareId;
|
|
25
|
+
}
|
|
26
|
+
export function normalizeClaudeSessionId(sessionId) {
|
|
27
|
+
return sessionId.startsWith("claude:") ? sessionId.slice("claude:".length) : sessionId;
|
|
28
|
+
}
|
|
29
|
+
export function encodeClaudeCwd(cwd) {
|
|
30
|
+
return cwd.replace(/[/. ]/g, "-");
|
|
31
|
+
}
|
|
32
|
+
export function claudeTranscriptPath(options) {
|
|
33
|
+
return join(options.home ?? agentHome(), ".claude", "projects", encodeClaudeCwd(options.cwd), `${assertSafeAgentSessionId(normalizeClaudeSessionId(options.sessionId))}.jsonl`);
|
|
34
|
+
}
|
|
35
|
+
export function normalizeCodexSessionId(sessionId) {
|
|
36
|
+
return sessionId.startsWith("codex:") ? sessionId.slice("codex:".length) : sessionId;
|
|
37
|
+
}
|
|
38
|
+
// Codex injects harness boilerplate as leading user-role turns: the project
|
|
39
|
+
// AGENTS.md (`# AGENTS.md instructions for …`, wrapping `<INSTRUCTIONS>`), the
|
|
40
|
+
// `<environment_context>` block, and `<user_instructions>`. None are human
|
|
41
|
+
// conversation — they precede the real first message and must not become a
|
|
42
|
+
// title or render as a user bubble.
|
|
43
|
+
const CODEX_INJECTED_PREFIXES = ["# AGENTS.md instructions for ", "<environment_context>", "<user_instructions>"];
|
|
44
|
+
export function isCodexInjectedContext(raw) {
|
|
45
|
+
const text = raw.trimStart();
|
|
46
|
+
return CODEX_INJECTED_PREFIXES.some((prefix) => text.startsWith(prefix));
|
|
47
|
+
}
|
|
48
|
+
export function codexTranscriptPath(options) {
|
|
49
|
+
const home = options.home ?? agentHome();
|
|
50
|
+
const sessionsRoot = join(home, ".codex", "sessions");
|
|
51
|
+
const codexSessionId = assertSafeAgentSessionId(normalizeCodexSessionId(options.sessionId));
|
|
52
|
+
return findCodexTranscriptById(sessionsRoot, codexSessionId);
|
|
53
|
+
}
|
|
54
|
+
export function readLocalClaudeSession(options) {
|
|
55
|
+
const transcriptPath = resolveClaudeTranscriptPath(options);
|
|
56
|
+
if (!transcriptPath) {
|
|
57
|
+
return emptyClaudeSession(options);
|
|
58
|
+
}
|
|
59
|
+
return parseClaudeSessionJsonl({
|
|
60
|
+
jsonl: readFileSync(transcriptPath, "utf8"),
|
|
61
|
+
sessionId: options.sessionId,
|
|
62
|
+
transcriptPath
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// Resolve a Claude transcript by id, searching every project dir (so an agent's
|
|
66
|
+
// working dir need not match the caller's). Returns undefined when no transcript
|
|
67
|
+
// exists yet — which is not an error: a freshly-launched agent has a session id
|
|
68
|
+
// but writes its transcript only once the conversation produces messages.
|
|
69
|
+
export function resolveClaudeTranscriptPath(options) {
|
|
70
|
+
const expectedTranscriptPath = claudeTranscriptPath(options);
|
|
71
|
+
if (existsSync(expectedTranscriptPath)) {
|
|
72
|
+
return expectedTranscriptPath;
|
|
73
|
+
}
|
|
74
|
+
const found = findClaudeTranscriptById(join(options.home ?? agentHome(), ".claude", "projects"), assertSafeAgentSessionId(normalizeClaudeSessionId(options.sessionId)));
|
|
75
|
+
return existsSync(found) ? found : undefined;
|
|
76
|
+
}
|
|
77
|
+
// A real, openable chat for a session that has no transcript yet — just an empty
|
|
78
|
+
// conversation. Parsing empty JSONL yields a well-formed session with zero
|
|
79
|
+
// messages, so a just-launched agent displays as an empty chat instead of 500ing.
|
|
80
|
+
export function emptyClaudeSession(options) {
|
|
81
|
+
return parseClaudeSessionJsonl({ jsonl: "", sessionId: options.sessionId, transcriptPath: claudeTranscriptPath(options) });
|
|
82
|
+
}
|
|
83
|
+
export function readLocalAgentSession(options) {
|
|
84
|
+
if (options.sessionId.startsWith("codex:")) {
|
|
85
|
+
return readLocalCodexSession(options);
|
|
86
|
+
}
|
|
87
|
+
if (options.sessionId.startsWith("gemini:")) {
|
|
88
|
+
return readLocalGeminiSession(options);
|
|
89
|
+
}
|
|
90
|
+
return readLocalClaudeSession(options);
|
|
91
|
+
}
|
|
92
|
+
// Gemini transcripts live at ~/.gemini/tmp/<slug>/chats/session-*.jsonl, one per
|
|
93
|
+
// session, with the sessionId in the header (the filename is a short id, not the
|
|
94
|
+
// session id). Resolve by narrowing to the cwd's chats dir via projects.json, then
|
|
95
|
+
// matching the header sessionId; fall back to a full walk.
|
|
96
|
+
// See docs/architecture/gemini-sessions.md.
|
|
97
|
+
export function normalizeGeminiSessionId(sessionId) {
|
|
98
|
+
return sessionId.startsWith("gemini:") ? sessionId.slice("gemini:".length) : sessionId;
|
|
99
|
+
}
|
|
100
|
+
function geminiChatsDir(options) {
|
|
101
|
+
if (!options.cwd)
|
|
102
|
+
return undefined;
|
|
103
|
+
try {
|
|
104
|
+
const raw = JSON.parse(readFileSync(join(options.home ?? agentHome(), ".gemini", "projects.json"), "utf8"));
|
|
105
|
+
const slug = raw.projects?.[options.cwd];
|
|
106
|
+
if (slug)
|
|
107
|
+
return join(options.home ?? agentHome(), ".gemini", "tmp", slug, "chats");
|
|
108
|
+
}
|
|
109
|
+
catch { /* no projects.json — fall back to a full walk */ }
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
function geminiHeaderSessionId(transcriptPath) {
|
|
113
|
+
const fd = openSync(transcriptPath, "r");
|
|
114
|
+
try {
|
|
115
|
+
// Read until the first newline rather than a fixed buffer — a header line that
|
|
116
|
+
// exceeded 8KB used to truncate, fail to parse, and silently show an empty
|
|
117
|
+
// chat. Cap the scan so a newline-less file can't be read unboundedly, and
|
|
118
|
+
// search for the newline at the byte level so a multibyte char can't split.
|
|
119
|
+
const maxHeaderBytes = 1_048_576;
|
|
120
|
+
const chunk = Buffer.alloc(65536);
|
|
121
|
+
const parts = [];
|
|
122
|
+
let position = 0;
|
|
123
|
+
while (position < maxHeaderBytes) {
|
|
124
|
+
const read = readSync(fd, chunk, 0, chunk.length, position);
|
|
125
|
+
if (read === 0)
|
|
126
|
+
break;
|
|
127
|
+
const slice = chunk.subarray(0, read);
|
|
128
|
+
const newline = slice.indexOf(0x0a);
|
|
129
|
+
if (newline >= 0) {
|
|
130
|
+
parts.push(slice.subarray(0, newline));
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
parts.push(Buffer.from(slice));
|
|
134
|
+
position += read;
|
|
135
|
+
}
|
|
136
|
+
const header = JSON.parse(Buffer.concat(parts).toString("utf8"));
|
|
137
|
+
return typeof header.sessionId === "string" ? header.sessionId : undefined;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
closeSync(fd);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
export function resolveGeminiTranscriptPath(options) {
|
|
147
|
+
const bareId = assertSafeAgentSessionId(normalizeGeminiSessionId(options.sessionId));
|
|
148
|
+
const tmpRoot = join(options.home ?? agentHome(), ".gemini", "tmp");
|
|
149
|
+
const narrowed = geminiChatsDir(options);
|
|
150
|
+
const roots = narrowed && existsSync(narrowed) ? [narrowed, tmpRoot] : [tmpRoot];
|
|
151
|
+
const seen = new Set();
|
|
152
|
+
for (const root of roots) {
|
|
153
|
+
for (const transcriptPath of walkJsonl(root)) {
|
|
154
|
+
if (seen.has(transcriptPath))
|
|
155
|
+
continue;
|
|
156
|
+
seen.add(transcriptPath);
|
|
157
|
+
if (geminiHeaderSessionId(transcriptPath) === bareId)
|
|
158
|
+
return transcriptPath;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
export function readLocalGeminiSession(options) {
|
|
164
|
+
const transcriptPath = resolveGeminiTranscriptPath(options);
|
|
165
|
+
const sessionId = options.sessionId.includes(":") ? options.sessionId : `gemini:${options.sessionId}`;
|
|
166
|
+
if (!transcriptPath) {
|
|
167
|
+
return { agentSessionId: sessionId, endOfTurn: false, lastAssistantText: "", messages: [], provider: "gemini", sessionId, timeline: [] };
|
|
168
|
+
}
|
|
169
|
+
return parseGeminiSessionJsonl({ jsonl: readFileSync(transcriptPath, "utf8"), sessionId, transcriptPath });
|
|
170
|
+
}
|
|
171
|
+
export function geminiContentText(content) {
|
|
172
|
+
if (typeof content === "string")
|
|
173
|
+
return content;
|
|
174
|
+
if (Array.isArray(content)) {
|
|
175
|
+
return content
|
|
176
|
+
.map((part) => (part && typeof part === "object" && typeof part.text === "string" ? part.text : ""))
|
|
177
|
+
.join(" ")
|
|
178
|
+
.trim();
|
|
179
|
+
}
|
|
180
|
+
return "";
|
|
181
|
+
}
|
|
182
|
+
export function parseGeminiSessionJsonl(options) {
|
|
183
|
+
const sessionId = options.sessionId.includes(":") ? options.sessionId : `gemini:${options.sessionId}`;
|
|
184
|
+
const messages = [];
|
|
185
|
+
const timeline = [];
|
|
186
|
+
for (const line of options.jsonl.split("\n")) {
|
|
187
|
+
if (!line.trim())
|
|
188
|
+
continue;
|
|
189
|
+
let entry;
|
|
190
|
+
try {
|
|
191
|
+
entry = JSON.parse(line);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (entry.type !== "user" && entry.type !== "gemini")
|
|
197
|
+
continue;
|
|
198
|
+
const timestamp = typeof entry.timestamp === "string" ? entry.timestamp : undefined;
|
|
199
|
+
if (entry.type === "gemini" && Array.isArray(entry.thoughts)) {
|
|
200
|
+
const thought = entry.thoughts
|
|
201
|
+
.map((item) => (typeof item?.description === "string" ? item.description : typeof item?.subject === "string" ? item.subject : ""))
|
|
202
|
+
.filter(Boolean)
|
|
203
|
+
.join("\n");
|
|
204
|
+
if (thought)
|
|
205
|
+
timeline.push({ kind: "gemini_thinking", text: thought, ...(timestamp ? { timestamp } : {}) });
|
|
206
|
+
}
|
|
207
|
+
const text = geminiContentText(entry.content);
|
|
208
|
+
if (!text)
|
|
209
|
+
continue;
|
|
210
|
+
const role = entry.type === "user" ? "user" : "assistant";
|
|
211
|
+
messages.push({ role, text, ...(timestamp ? { timestamp } : {}) });
|
|
212
|
+
timeline.push({ kind: entry.type === "user" ? "gemini_user_message" : "gemini_assistant_message", text, ...(timestamp ? { timestamp } : {}) });
|
|
213
|
+
}
|
|
214
|
+
const assistantMessages = messages.filter((message) => message.role === "assistant");
|
|
215
|
+
const firstAssistantMessage = assistantMessages[0];
|
|
216
|
+
const lastAssistantMessage = assistantMessages[assistantMessages.length - 1];
|
|
217
|
+
const firstMessage = messages[0];
|
|
218
|
+
const lastMessage = messages[messages.length - 1];
|
|
219
|
+
return {
|
|
220
|
+
agentSessionId: sessionId,
|
|
221
|
+
endOfTurn: lastMessage?.role === "assistant",
|
|
222
|
+
...(firstAssistantMessage ? { firstAssistantMessage } : {}),
|
|
223
|
+
...(firstMessage ? { firstMessage } : {}),
|
|
224
|
+
...(lastAssistantMessage ? { lastAssistantMessage } : {}),
|
|
225
|
+
lastAssistantText: lastAssistantMessage?.text ?? "",
|
|
226
|
+
...(lastMessage ? { lastMessage } : {}),
|
|
227
|
+
messages,
|
|
228
|
+
provider: "gemini",
|
|
229
|
+
sessionId,
|
|
230
|
+
timeline,
|
|
231
|
+
...(options.transcriptPath ? { transcriptPath: options.transcriptPath } : {})
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
// Async twin used by the per-request session-detail path (HTTP open-a-chat,
|
|
235
|
+
// socket subscribe ack). The full transcript read is the only large I/O — path
|
|
236
|
+
// resolution is bounded (stat + first-line reads) — so reading it async keeps a
|
|
237
|
+
// multi-megabyte transcript open from blocking the provider event loop. Parsing
|
|
238
|
+
// is unavoidable CPU work but one-shot per open.
|
|
239
|
+
export async function readLocalAgentSessionAsync(options) {
|
|
240
|
+
// A subagent sidechain, addressed by its composite id (claude-subagent:…).
|
|
241
|
+
// Read directly; it has no subagents of its own (no nesting).
|
|
242
|
+
const subagentRef = parseSubagentSessionId(options.sessionId);
|
|
243
|
+
if (subagentRef) {
|
|
244
|
+
const transcriptPath = resolveClaudeSubagentTranscriptPath({ ...(options.home ? { home: options.home } : {}), ...subagentRef });
|
|
245
|
+
return parseClaudeSessionJsonl({ jsonl: await readFileAsync(transcriptPath, "utf8"), sessionId: options.sessionId, transcriptPath });
|
|
246
|
+
}
|
|
247
|
+
if (options.sessionId.startsWith("codex:")) {
|
|
248
|
+
const transcriptPath = codexTranscriptPath(options);
|
|
249
|
+
if (!transcriptPath) {
|
|
250
|
+
throw new Error(`Codex session transcript was not found for ${options.sessionId} in ${options.cwd}`);
|
|
251
|
+
}
|
|
252
|
+
return parseCodexSessionJsonl({ jsonl: await readFileAsync(transcriptPath, "utf8"), sessionId: options.sessionId, transcriptPath });
|
|
253
|
+
}
|
|
254
|
+
if (options.sessionId.startsWith("gemini:")) {
|
|
255
|
+
const transcriptPath = resolveGeminiTranscriptPath(options);
|
|
256
|
+
if (!transcriptPath) {
|
|
257
|
+
return readLocalGeminiSession(options);
|
|
258
|
+
}
|
|
259
|
+
return parseGeminiSessionJsonl({ jsonl: await readFileAsync(transcriptPath, "utf8"), sessionId: options.sessionId, transcriptPath });
|
|
260
|
+
}
|
|
261
|
+
const transcriptPath = resolveClaudeTranscriptPath(options);
|
|
262
|
+
if (!transcriptPath) {
|
|
263
|
+
return emptyClaudeSession(options);
|
|
264
|
+
}
|
|
265
|
+
const details = parseClaudeSessionJsonl({ jsonl: await readFileAsync(transcriptPath, "utf8"), sessionId: options.sessionId, transcriptPath });
|
|
266
|
+
const subagents = listLocalAgentSubagents({ ...(options.home ? { home: options.home } : {}), sessionId: options.sessionId, transcriptPath });
|
|
267
|
+
return subagents.length ? { ...details, subagents } : details;
|
|
268
|
+
}
|
|
269
|
+
// Composite session id for a subagent sidechain, so the existing watcher/socket
|
|
270
|
+
// (keyed by an opaque session id) can stream it live like any other session:
|
|
271
|
+
// claude-subagent:<parent-session-id>:<agent-id>
|
|
272
|
+
const CLAUDE_SUBAGENT_PREFIX = "claude-subagent:";
|
|
273
|
+
export function makeSubagentSessionId(parentSessionId, agentId) {
|
|
274
|
+
return `${CLAUDE_SUBAGENT_PREFIX}${normalizeClaudeSessionId(parentSessionId)}:${agentId}`;
|
|
275
|
+
}
|
|
276
|
+
export function parseSubagentSessionId(sessionId) {
|
|
277
|
+
if (!sessionId.startsWith(CLAUDE_SUBAGENT_PREFIX))
|
|
278
|
+
return undefined;
|
|
279
|
+
const rest = sessionId.slice(CLAUDE_SUBAGENT_PREFIX.length);
|
|
280
|
+
const separator = rest.indexOf(":");
|
|
281
|
+
if (separator <= 0 || separator === rest.length - 1)
|
|
282
|
+
return undefined;
|
|
283
|
+
return { agentId: rest.slice(separator + 1), parentSessionId: rest.slice(0, separator) };
|
|
284
|
+
}
|
|
285
|
+
// Resolve a subagent sidechain to its file. The agent id is constrained to the
|
|
286
|
+
// file-name alphabet so it can never escape the resolved subagents directory.
|
|
287
|
+
export function resolveClaudeSubagentTranscriptPath(options) {
|
|
288
|
+
if (!/^[A-Za-z0-9_-]+$/.test(options.agentId)) {
|
|
289
|
+
throw new Error(`Invalid subagent id: ${options.agentId}`);
|
|
290
|
+
}
|
|
291
|
+
const sessionId = options.parentSessionId ?? options.sessionId ?? "";
|
|
292
|
+
const dir = claudeSubagentsDir({ ...(options.home ? { home: options.home } : {}), sessionId });
|
|
293
|
+
if (!dir) {
|
|
294
|
+
throw new Error(`No subagents found for session ${sessionId}`);
|
|
295
|
+
}
|
|
296
|
+
const transcriptPath = join(dir, `agent-${options.agentId}.jsonl`);
|
|
297
|
+
if (!existsSync(transcriptPath)) {
|
|
298
|
+
throw new Error(`Subagent transcript was not found: ${options.agentId}`);
|
|
299
|
+
}
|
|
300
|
+
return transcriptPath;
|
|
301
|
+
}
|
|
302
|
+
// The subagents directory for a Claude session: a `subagents/` folder beside
|
|
303
|
+
// the session's own `<id>/` directory. The transcript path is reused when known
|
|
304
|
+
// (the open-a-chat path resolves it anyway); otherwise the parent is found by id.
|
|
305
|
+
function claudeSubagentsDir(options) {
|
|
306
|
+
const bareId = assertSafeAgentSessionId(normalizeClaudeSessionId(options.sessionId));
|
|
307
|
+
const transcriptPath = options.transcriptPath
|
|
308
|
+
?? findClaudeTranscriptById(join(options.home ?? agentHome(), ".claude", "projects"), bareId);
|
|
309
|
+
if (!existsSync(transcriptPath))
|
|
310
|
+
return undefined;
|
|
311
|
+
const dir = join(dirname(transcriptPath), bareId, "subagents");
|
|
312
|
+
return existsSync(dir) ? dir : undefined;
|
|
313
|
+
}
|
|
314
|
+
// List a session's subagent sidechains by id + mtime (oldest first, i.e. spawn
|
|
315
|
+
// order). Metadata only — never reads transcript content — so opening a chat
|
|
316
|
+
// stays cheap regardless of how many subagents it spawned.
|
|
317
|
+
export function listLocalAgentSubagents(options) {
|
|
318
|
+
const dir = claudeSubagentsDir(options);
|
|
319
|
+
if (!dir)
|
|
320
|
+
return [];
|
|
321
|
+
const refs = [];
|
|
322
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
323
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl") || !entry.name.startsWith("agent-"))
|
|
324
|
+
continue;
|
|
325
|
+
const agentId = entry.name.slice("agent-".length, -".jsonl".length);
|
|
326
|
+
if (!agentId)
|
|
327
|
+
continue;
|
|
328
|
+
refs.push({ agentId, updatedAt: new Date(statSync(join(dir, entry.name)).mtimeMs).toISOString() });
|
|
329
|
+
}
|
|
330
|
+
return refs.sort((a, b) => a.updatedAt.localeCompare(b.updatedAt) || a.agentId.localeCompare(b.agentId));
|
|
331
|
+
}
|
|
332
|
+
// Read one subagent's full sidechain transcript on demand. The agent id is
|
|
333
|
+
// constrained to the file-name alphabet so it can never escape the resolved
|
|
334
|
+
// subagents directory.
|
|
335
|
+
export async function readLocalAgentSubagentSessionAsync(options) {
|
|
336
|
+
const transcriptPath = resolveClaudeSubagentTranscriptPath(options);
|
|
337
|
+
return parseClaudeSessionJsonl({
|
|
338
|
+
jsonl: await readFileAsync(transcriptPath, "utf8"),
|
|
339
|
+
sessionId: `claude:${options.agentId}`,
|
|
340
|
+
transcriptPath
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
export function readLocalCodexSession(options) {
|
|
344
|
+
const transcriptPath = codexTranscriptPath(options);
|
|
345
|
+
if (!transcriptPath) {
|
|
346
|
+
throw new Error(`Codex session transcript was not found for ${options.sessionId} in ${options.cwd}`);
|
|
347
|
+
}
|
|
348
|
+
return parseCodexSessionJsonl({
|
|
349
|
+
jsonl: readFileSync(transcriptPath, "utf8"),
|
|
350
|
+
sessionId: options.sessionId,
|
|
351
|
+
transcriptPath
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
function messageSummaryFields(messages) {
|
|
355
|
+
const summaries = messages.map((message) => ({
|
|
356
|
+
role: message.role,
|
|
357
|
+
text: message.text,
|
|
358
|
+
...(message.timestamp ? { timestamp: message.timestamp } : {})
|
|
359
|
+
}));
|
|
360
|
+
const assistantSummaries = summaries.filter((message) => message.role === "assistant");
|
|
361
|
+
return {
|
|
362
|
+
...(assistantSummaries[0] ? { firstAssistantMessage: assistantSummaries[0] } : {}),
|
|
363
|
+
...(summaries[0] ? { firstMessage: summaries[0] } : {}),
|
|
364
|
+
...(assistantSummaries.length ? { lastAssistantMessage: assistantSummaries[assistantSummaries.length - 1] } : {}),
|
|
365
|
+
...(summaries.length ? { lastMessage: summaries[summaries.length - 1] } : {})
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
// Incremental fold over transcript lines: feedLine accumulates parser state
|
|
369
|
+
// one JSONL line at a time, finalize snapshots the current details. Feeding
|
|
370
|
+
// further lines after a finalize is supported (tail readers rely on it); the
|
|
371
|
+
// snapshot copies the top-level arrays, but a later tool-result line may
|
|
372
|
+
// still attach to a timeline item shared with an earlier snapshot, so
|
|
373
|
+
// consumers should serialize a snapshot before the next feed.
|
|
374
|
+
export function createClaudeSessionParser(options) {
|
|
375
|
+
const messages = [];
|
|
376
|
+
const timeline = [];
|
|
377
|
+
const toolCallsById = new Map();
|
|
378
|
+
let lastAssistantMessage;
|
|
379
|
+
return {
|
|
380
|
+
feedLine(line) {
|
|
381
|
+
if (!line.trim())
|
|
382
|
+
return;
|
|
383
|
+
let entry;
|
|
384
|
+
try {
|
|
385
|
+
entry = JSON.parse(line);
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const metadataItem = timelineItemFromMetadataEntry(entry);
|
|
391
|
+
if (metadataItem) {
|
|
392
|
+
timeline.push(metadataItem);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (entry.type !== "assistant" && entry.type !== "user")
|
|
396
|
+
return;
|
|
397
|
+
const message = entry.message;
|
|
398
|
+
if (!message || typeof message !== "object" || Array.isArray(message))
|
|
399
|
+
return;
|
|
400
|
+
const messageRecord = message;
|
|
401
|
+
const blocks = blocksFromContent(messageRecord.content);
|
|
402
|
+
const text = blocks.map((block) => blockText(block)).filter(Boolean).join("\n\n");
|
|
403
|
+
if (!text)
|
|
404
|
+
return;
|
|
405
|
+
const conversationalBlocks = conversationalClaudeBlocks({ blocks, role: entry.type });
|
|
406
|
+
if (conversationalBlocks.length > 0) {
|
|
407
|
+
const conversationalMessage = {
|
|
408
|
+
blocks: conversationalBlocks,
|
|
409
|
+
role: entry.type,
|
|
410
|
+
text: conversationalBlocks.map((block) => blockText(block)).filter(Boolean).join("\n\n"),
|
|
411
|
+
...(typeof messageRecord.stop_reason === "string" ? { stopReason: messageRecord.stop_reason } : {}),
|
|
412
|
+
...(typeof entry.timestamp === "string" ? { timestamp: entry.timestamp } : {})
|
|
413
|
+
};
|
|
414
|
+
messages.push(conversationalMessage);
|
|
415
|
+
if (conversationalMessage.role === "assistant") {
|
|
416
|
+
lastAssistantMessage = conversationalMessage;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
timeline.push(...timelineItemsFromMessage({
|
|
420
|
+
blocks,
|
|
421
|
+
entry,
|
|
422
|
+
role: entry.type,
|
|
423
|
+
timeline,
|
|
424
|
+
toolCallsById
|
|
425
|
+
}));
|
|
426
|
+
},
|
|
427
|
+
finalize() {
|
|
428
|
+
const agentSessionId = normalizeClaudeSessionId(options.sessionId);
|
|
429
|
+
const summary = messageSummaryFields(messages);
|
|
430
|
+
return {
|
|
431
|
+
agentSessionId,
|
|
432
|
+
endOfTurn: lastAssistantMessage?.stopReason === "end_turn",
|
|
433
|
+
...summary,
|
|
434
|
+
lastAssistantText: lastAssistantMessage?.text ?? "",
|
|
435
|
+
messages: messages.slice(),
|
|
436
|
+
provider: "claude",
|
|
437
|
+
sessionId: `claude:${agentSessionId}`,
|
|
438
|
+
timeline: timeline.slice(),
|
|
439
|
+
...(options.transcriptPath ? { transcriptPath: options.transcriptPath } : {})
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
export function parseClaudeSessionJsonl(options) {
|
|
445
|
+
const parser = createClaudeSessionParser(options);
|
|
446
|
+
for (const line of options.jsonl.split("\n")) {
|
|
447
|
+
parser.feedLine(line);
|
|
448
|
+
}
|
|
449
|
+
return parser.finalize();
|
|
450
|
+
}
|
|
451
|
+
function conversationalClaudeBlocks({ blocks, role }) {
|
|
452
|
+
if (role === "user") {
|
|
453
|
+
return blocks.every((block) => block.kind === "text") ? blocks : [];
|
|
454
|
+
}
|
|
455
|
+
return blocks.filter((block) => block.kind === "text" || block.kind === "thinking");
|
|
456
|
+
}
|
|
457
|
+
function codexTimelineItemFromResponsePayload(payload, timestamp, callsById, timeline) {
|
|
458
|
+
const payloadType = typeof payload.type === "string" ? payload.type : "";
|
|
459
|
+
if (payloadType === "message") {
|
|
460
|
+
const role = typeof payload.role === "string" ? payload.role : "assistant";
|
|
461
|
+
const text = codexContentText(payload.content);
|
|
462
|
+
if (!text)
|
|
463
|
+
return undefined;
|
|
464
|
+
if (role === "user") {
|
|
465
|
+
// Codex prepends synthetic user turns (AGENTS.md, the environment context
|
|
466
|
+
// block, user instructions). They are harness boilerplate, not human
|
|
467
|
+
// conversation — render them as dimmed context, never as a user bubble.
|
|
468
|
+
if (isCodexInjectedContext(text)) {
|
|
469
|
+
return { kind: "codex_context", label: "Context", text, ...(timestamp ? { timestamp } : {}) };
|
|
470
|
+
}
|
|
471
|
+
return { kind: "codex_user_message", text, ...(timestamp ? { timestamp } : {}) };
|
|
472
|
+
}
|
|
473
|
+
if (role === "assistant") {
|
|
474
|
+
return {
|
|
475
|
+
kind: "codex_assistant_message",
|
|
476
|
+
phase: typeof payload.phase === "string" ? payload.phase : undefined,
|
|
477
|
+
text,
|
|
478
|
+
...(timestamp ? { timestamp } : {})
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
kind: "codex_context",
|
|
483
|
+
label: role,
|
|
484
|
+
text,
|
|
485
|
+
...(timestamp ? { timestamp } : {})
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
if (payloadType === "reasoning") {
|
|
489
|
+
const summary = codexReasoningSummary(payload.summary);
|
|
490
|
+
const text = typeof payload.content === "string" && payload.content.trim()
|
|
491
|
+
? payload.content
|
|
492
|
+
: summary;
|
|
493
|
+
if (!text)
|
|
494
|
+
return undefined;
|
|
495
|
+
return { kind: "codex_reasoning", ...(summary ? { summary } : {}), text, ...(timestamp ? { timestamp } : {}) };
|
|
496
|
+
}
|
|
497
|
+
if (payloadType === "web_search_call") {
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
if (payloadType === "function_call" || payloadType === "custom_tool_call" || payloadType === "tool_search_call") {
|
|
501
|
+
const name = codexToolName(payload);
|
|
502
|
+
const callId = typeof payload.call_id === "string" ? payload.call_id : undefined;
|
|
503
|
+
const toolCategory = toolCategoryForCodexName(name);
|
|
504
|
+
const args = codexToolArguments(payload);
|
|
505
|
+
if (callId) {
|
|
506
|
+
callsById.set(callId, { arguments: args, name, timelineIndex: timeline.length, timestamp, toolCategory });
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
arguments: args,
|
|
510
|
+
...(callId ? { callId } : {}),
|
|
511
|
+
kind: "codex_tool_call",
|
|
512
|
+
name,
|
|
513
|
+
toolCategory,
|
|
514
|
+
...(timestamp ? { timestamp } : {})
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
if (payloadType === "function_call_output" || payloadType === "custom_tool_call_output" || payloadType === "tool_search_output") {
|
|
518
|
+
const callId = typeof payload.call_id === "string" ? payload.call_id : undefined;
|
|
519
|
+
const call = callId ? callsById.get(callId) : undefined;
|
|
520
|
+
const output = codexToolOutput(payload);
|
|
521
|
+
if (call?.timelineIndex !== undefined) {
|
|
522
|
+
const existing = timeline[call.timelineIndex];
|
|
523
|
+
if (existing?.kind === "codex_tool_call") {
|
|
524
|
+
timeline[call.timelineIndex] = {
|
|
525
|
+
...(callId ? { callId } : {}),
|
|
526
|
+
kind: "codex_tool_exchange",
|
|
527
|
+
result: {
|
|
528
|
+
output,
|
|
529
|
+
...(typeof payload.status === "string" ? { status: payload.status } : {}),
|
|
530
|
+
...(timestamp ? { timestamp } : {})
|
|
531
|
+
},
|
|
532
|
+
toolUse: {
|
|
533
|
+
arguments: existing.arguments,
|
|
534
|
+
name: existing.name,
|
|
535
|
+
toolCategory: existing.toolCategory,
|
|
536
|
+
...(existing.timestamp ? { timestamp: existing.timestamp } : {})
|
|
537
|
+
},
|
|
538
|
+
...(existing.timestamp ? { timestamp: existing.timestamp } : timestamp ? { timestamp } : {})
|
|
539
|
+
};
|
|
540
|
+
return undefined;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
...(callId ? { callId } : {}),
|
|
545
|
+
kind: "codex_tool_result",
|
|
546
|
+
...(call?.name ? { name: call.name } : {}),
|
|
547
|
+
output,
|
|
548
|
+
...(typeof payload.status === "string" ? { status: payload.status } : {}),
|
|
549
|
+
toolCategory: call?.toolCategory ?? "generic",
|
|
550
|
+
...(timestamp ? { timestamp } : {})
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
return undefined;
|
|
554
|
+
}
|
|
555
|
+
function codexTimelineItemFromEventPayload(payload, timestamp) {
|
|
556
|
+
const type = typeof payload.type === "string" ? payload.type : "";
|
|
557
|
+
if (type === "token_count")
|
|
558
|
+
return undefined;
|
|
559
|
+
if (type === "task_started") {
|
|
560
|
+
return {
|
|
561
|
+
kind: "codex_task_event",
|
|
562
|
+
label: "Task started",
|
|
563
|
+
text: summarizeKeyValues(payload, ["turn_id", "model_context_window", "collaboration_mode_kind", "started_at"]),
|
|
564
|
+
...(timestamp ? { timestamp } : {})
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
if (type === "task_complete") {
|
|
568
|
+
return {
|
|
569
|
+
kind: "codex_task_event",
|
|
570
|
+
label: "Task complete",
|
|
571
|
+
text: summarizeKeyValues(payload, ["duration_ms", "time_to_first_token_ms", "last_agent_message"]),
|
|
572
|
+
tone: "success",
|
|
573
|
+
...(timestampFromPayload(payload, "completed_at") ?? timestamp ? { timestamp: timestampFromPayload(payload, "completed_at") ?? timestamp } : {})
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
if (type === "turn_aborted") {
|
|
577
|
+
return {
|
|
578
|
+
kind: "codex_task_event",
|
|
579
|
+
label: "Turn aborted",
|
|
580
|
+
text: typeof payload.reason === "string" ? payload.reason : safeJson(payload),
|
|
581
|
+
tone: "warning",
|
|
582
|
+
...(timestampFromPayload(payload, "completed_at") ?? timestamp ? { timestamp: timestampFromPayload(payload, "completed_at") ?? timestamp } : {})
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
if (type === "context_compacted") {
|
|
586
|
+
return { kind: "codex_compaction", text: "Context compacted.", ...(timestamp ? { timestamp } : {}) };
|
|
587
|
+
}
|
|
588
|
+
if (type === "exec_command_end") {
|
|
589
|
+
return codexToolCompletionEvent(payload, "exec_command", timestamp);
|
|
590
|
+
}
|
|
591
|
+
if (type === "patch_apply_end") {
|
|
592
|
+
return {
|
|
593
|
+
...(typeof payload.call_id === "string" ? { callId: payload.call_id } : {}),
|
|
594
|
+
changes: codexPatchChanges(payload.changes),
|
|
595
|
+
kind: "codex_patch",
|
|
596
|
+
output: [payload.stdout, payload.stderr].filter((value) => typeof value === "string" && value.trim().length > 0).join("\n") || safeJson(payload),
|
|
597
|
+
status: typeof payload.status === "string" ? payload.status : undefined,
|
|
598
|
+
success: payload.success === true,
|
|
599
|
+
...(timestamp ? { timestamp } : {})
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
if (type === "mcp_tool_call_end") {
|
|
603
|
+
return codexToolCompletionEvent(payload, "mcp_tool", timestamp);
|
|
604
|
+
}
|
|
605
|
+
if (type === "web_search_end") {
|
|
606
|
+
return {
|
|
607
|
+
...(typeof payload.call_id === "string" ? { callId: payload.call_id } : {}),
|
|
608
|
+
kind: "codex_web_search",
|
|
609
|
+
query: codexWebSearchQuery(payload),
|
|
610
|
+
...(timestamp ? { timestamp } : {})
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
if (type === "view_image_tool_call") {
|
|
614
|
+
return {
|
|
615
|
+
arguments: typeof payload.path === "string" ? payload.path : safeJson(payload),
|
|
616
|
+
...(typeof payload.call_id === "string" ? { callId: payload.call_id } : {}),
|
|
617
|
+
kind: "codex_tool_call",
|
|
618
|
+
name: "view_image",
|
|
619
|
+
toolCategory: "file_read",
|
|
620
|
+
...(timestamp ? { timestamp } : {})
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
if (type === "thread_goal_updated") {
|
|
624
|
+
const goal = payload.goal && typeof payload.goal === "object" ? payload.goal : undefined;
|
|
625
|
+
return {
|
|
626
|
+
kind: "codex_goal",
|
|
627
|
+
status: typeof goal?.status === "string" ? goal.status : undefined,
|
|
628
|
+
text: goal ? summarizeCodexGoal(goal) : safeJson(payload),
|
|
629
|
+
...(timestamp ? { timestamp } : {})
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
if (type === "entered_review_mode" || type === "exited_review_mode" || type === "item_completed") {
|
|
633
|
+
return {
|
|
634
|
+
kind: "codex_review",
|
|
635
|
+
text: summarizeCodexReview(payload),
|
|
636
|
+
...(timestamp ? { timestamp } : {})
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
if (type === "error") {
|
|
640
|
+
return {
|
|
641
|
+
kind: "codex_error",
|
|
642
|
+
text: typeof payload.message === "string" ? payload.message : safeJson(payload),
|
|
643
|
+
...(timestamp ? { timestamp } : {})
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
if (type === "agent_reasoning") {
|
|
647
|
+
return {
|
|
648
|
+
kind: "codex_reasoning",
|
|
649
|
+
text: typeof payload.text === "string" ? payload.text : safeJson(payload),
|
|
650
|
+
...(timestamp ? { timestamp } : {})
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
if (type.startsWith("collab_") || type === "thread_rolled_back") {
|
|
654
|
+
return {
|
|
655
|
+
kind: "codex_task_event",
|
|
656
|
+
label: codexEventLabel(type),
|
|
657
|
+
text: safeJson(payload),
|
|
658
|
+
tone: type.includes("end") ? "success" : "default",
|
|
659
|
+
...(timestamp ? { timestamp } : {})
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
return {
|
|
663
|
+
kind: "codex_task_event",
|
|
664
|
+
label: codexEventLabel(type || "Event"),
|
|
665
|
+
text: safeJson(payload),
|
|
666
|
+
...(timestamp ? { timestamp } : {})
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
function mergeCodexEventItemIntoCall({ callsById, item, timeline }) {
|
|
670
|
+
if (!("callId" in item) || !item.callId)
|
|
671
|
+
return false;
|
|
672
|
+
const call = callsById.get(item.callId);
|
|
673
|
+
const timelineIndex = call?.timelineIndex ?? timeline.findIndex((candidate) => "callId" in candidate && candidate.callId === item.callId);
|
|
674
|
+
if (timelineIndex < 0)
|
|
675
|
+
return false;
|
|
676
|
+
const existing = timeline[timelineIndex];
|
|
677
|
+
if (!existing)
|
|
678
|
+
return false;
|
|
679
|
+
if (item.kind === "codex_patch" && (call?.name === "apply_patch" || codexTimelineToolName(existing) === "apply_patch")) {
|
|
680
|
+
timeline[timelineIndex] = item;
|
|
681
|
+
return true;
|
|
682
|
+
}
|
|
683
|
+
if (item.kind === "codex_web_search" && (call?.name === "web_search" || codexTimelineToolName(existing) === "web_search")) {
|
|
684
|
+
timeline[timelineIndex] = item;
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
687
|
+
if (item.kind === "codex_tool_call") {
|
|
688
|
+
return existing.kind === "codex_tool_call" || existing.kind === "codex_tool_exchange";
|
|
689
|
+
}
|
|
690
|
+
if (item.kind !== "codex_tool_result")
|
|
691
|
+
return false;
|
|
692
|
+
if (existing.kind === "codex_tool_exchange") {
|
|
693
|
+
timeline[timelineIndex] = {
|
|
694
|
+
...existing,
|
|
695
|
+
result: {
|
|
696
|
+
output: item.output,
|
|
697
|
+
...(item.status ? { status: item.status } : {}),
|
|
698
|
+
...(item.timestamp ? { timestamp: item.timestamp } : {})
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
return true;
|
|
702
|
+
}
|
|
703
|
+
if (existing.kind === "codex_tool_call") {
|
|
704
|
+
timeline[timelineIndex] = {
|
|
705
|
+
...(item.callId ? { callId: item.callId } : {}),
|
|
706
|
+
kind: "codex_tool_exchange",
|
|
707
|
+
result: {
|
|
708
|
+
output: item.output,
|
|
709
|
+
...(item.status ? { status: item.status } : {}),
|
|
710
|
+
...(item.timestamp ? { timestamp: item.timestamp } : {})
|
|
711
|
+
},
|
|
712
|
+
toolUse: {
|
|
713
|
+
arguments: existing.arguments,
|
|
714
|
+
name: existing.name,
|
|
715
|
+
toolCategory: existing.toolCategory,
|
|
716
|
+
...(existing.timestamp ? { timestamp: existing.timestamp } : {})
|
|
717
|
+
},
|
|
718
|
+
...(existing.timestamp ? { timestamp: existing.timestamp } : item.timestamp ? { timestamp: item.timestamp } : {})
|
|
719
|
+
};
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
if (existing.kind === "codex_tool_result") {
|
|
723
|
+
timeline[timelineIndex] = item;
|
|
724
|
+
return true;
|
|
725
|
+
}
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
function codexTimelineToolName(item) {
|
|
729
|
+
if (item.kind === "codex_tool_call")
|
|
730
|
+
return item.name;
|
|
731
|
+
if (item.kind === "codex_tool_result")
|
|
732
|
+
return item.name;
|
|
733
|
+
if (item.kind === "codex_tool_exchange")
|
|
734
|
+
return item.toolUse.name;
|
|
735
|
+
return undefined;
|
|
736
|
+
}
|
|
737
|
+
export function parseCodexSessionJsonl(options) {
|
|
738
|
+
const messages = [];
|
|
739
|
+
const timeline = [];
|
|
740
|
+
const callsById = new Map();
|
|
741
|
+
let agentSessionId = normalizeCodexSessionId(options.sessionId);
|
|
742
|
+
let lastAssistantText = "";
|
|
743
|
+
let endOfTurn = false;
|
|
744
|
+
for (const line of options.jsonl.split("\n")) {
|
|
745
|
+
if (!line.trim())
|
|
746
|
+
continue;
|
|
747
|
+
let entry;
|
|
748
|
+
try {
|
|
749
|
+
entry = JSON.parse(line);
|
|
750
|
+
}
|
|
751
|
+
catch {
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
const timestamp = timestampFromEntry(entry);
|
|
755
|
+
const payload = entry.payload && typeof entry.payload === "object" ? entry.payload : undefined;
|
|
756
|
+
if (entry.type === "session_meta" && payload) {
|
|
757
|
+
if (typeof payload.id === "string")
|
|
758
|
+
agentSessionId = payload.id;
|
|
759
|
+
timeline.push({
|
|
760
|
+
cwd: typeof payload.cwd === "string" ? payload.cwd : undefined,
|
|
761
|
+
kind: "codex_context",
|
|
762
|
+
label: "Session",
|
|
763
|
+
model: typeof payload.model === "string" ? payload.model : undefined,
|
|
764
|
+
text: summarizeCodexSessionMeta(payload),
|
|
765
|
+
...(timestamp ? { timestamp } : {})
|
|
766
|
+
});
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
if (entry.type === "turn_context" && payload) {
|
|
770
|
+
timeline.push({
|
|
771
|
+
cwd: typeof payload.cwd === "string" ? payload.cwd : undefined,
|
|
772
|
+
kind: "codex_context",
|
|
773
|
+
label: "Turn context",
|
|
774
|
+
model: typeof payload.model === "string" ? payload.model : undefined,
|
|
775
|
+
text: summarizeCodexTurnContext(payload),
|
|
776
|
+
...(timestamp ? { timestamp } : {})
|
|
777
|
+
});
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
if (entry.type === "compacted" && payload) {
|
|
781
|
+
timeline.push({
|
|
782
|
+
kind: "codex_compaction",
|
|
783
|
+
text: typeof payload.message === "string" && payload.message.trim() ? payload.message : "Conversation compacted.",
|
|
784
|
+
...(timestamp ? { timestamp } : {})
|
|
785
|
+
});
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
if (!payload)
|
|
789
|
+
continue;
|
|
790
|
+
const payloadType = typeof payload.type === "string" ? payload.type : "";
|
|
791
|
+
if (entry.type === "response_item") {
|
|
792
|
+
const item = codexTimelineItemFromResponsePayload(payload, timestamp, callsById, timeline);
|
|
793
|
+
if (item) {
|
|
794
|
+
timeline.push(item);
|
|
795
|
+
if (item.kind === "codex_assistant_message") {
|
|
796
|
+
lastAssistantText = item.text;
|
|
797
|
+
messages.push({ role: "assistant", text: item.text, ...(timestamp ? { timestamp } : {}) });
|
|
798
|
+
}
|
|
799
|
+
else if (item.kind === "codex_user_message") {
|
|
800
|
+
messages.push({ role: "user", text: item.text, ...(timestamp ? { timestamp } : {}) });
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
if (entry.type === "event_msg") {
|
|
806
|
+
if (payloadType === "user_message") {
|
|
807
|
+
const text = typeof payload.message === "string" ? payload.message : safeJson(payload);
|
|
808
|
+
messages.push({ role: "user", text, ...(timestamp ? { timestamp } : {}) });
|
|
809
|
+
timeline.push({ kind: "codex_user_message", text, ...(timestamp ? { timestamp } : {}) });
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
if (payloadType === "agent_message") {
|
|
813
|
+
const text = typeof payload.message === "string" ? payload.message : safeJson(payload);
|
|
814
|
+
lastAssistantText = text;
|
|
815
|
+
timeline.push({
|
|
816
|
+
kind: "codex_commentary",
|
|
817
|
+
phase: typeof payload.phase === "string" ? payload.phase : undefined,
|
|
818
|
+
text,
|
|
819
|
+
...(timestamp ? { timestamp } : {})
|
|
820
|
+
});
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
const eventItem = codexTimelineItemFromEventPayload(payload, timestamp);
|
|
824
|
+
if (eventItem) {
|
|
825
|
+
if (mergeCodexEventItemIntoCall({ callsById, item: eventItem, timeline })) {
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
timeline.push(eventItem);
|
|
829
|
+
if (eventItem.kind === "codex_task_event" && eventItem.label === "Task complete") {
|
|
830
|
+
endOfTurn = true;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
const summary = messageSummaryFields(messages);
|
|
836
|
+
return {
|
|
837
|
+
agentSessionId,
|
|
838
|
+
endOfTurn,
|
|
839
|
+
...summary,
|
|
840
|
+
lastAssistantText,
|
|
841
|
+
messages,
|
|
842
|
+
provider: "codex",
|
|
843
|
+
sessionId: `codex:${agentSessionId}`,
|
|
844
|
+
timeline,
|
|
845
|
+
...(options.transcriptPath ? { transcriptPath: options.transcriptPath } : {})
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
function timelineItemsFromMessage({ blocks, entry, role, timeline, toolCallsById }) {
|
|
849
|
+
const timestamp = typeof entry.timestamp === "string" ? entry.timestamp : undefined;
|
|
850
|
+
if (role === "user" && blocks.every((block) => block.kind === "text")) {
|
|
851
|
+
const text = blocks.map((block) => blockText(block)).join("\n\n");
|
|
852
|
+
const interruption = claudeInterruptionEvent(text, timestamp);
|
|
853
|
+
if (interruption)
|
|
854
|
+
return [interruption];
|
|
855
|
+
return [{
|
|
856
|
+
kind: "claude_user_message",
|
|
857
|
+
text,
|
|
858
|
+
...(timestamp ? { timestamp } : {})
|
|
859
|
+
}];
|
|
860
|
+
}
|
|
861
|
+
if (role === "assistant" && blocks.every((block) => block.kind === "text")) {
|
|
862
|
+
const text = blocks.map((block) => blockText(block)).join("\n\n");
|
|
863
|
+
return [{
|
|
864
|
+
blocks,
|
|
865
|
+
kind: "claude_assistant_message",
|
|
866
|
+
text,
|
|
867
|
+
...(timestamp ? { timestamp } : {})
|
|
868
|
+
}];
|
|
869
|
+
}
|
|
870
|
+
const items = [];
|
|
871
|
+
const assistantTextBlocks = [];
|
|
872
|
+
for (const block of blocks) {
|
|
873
|
+
if (role === "assistant" && block.kind === "text") {
|
|
874
|
+
assistantTextBlocks.push(block);
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
if (assistantTextBlocks.length > 0) {
|
|
878
|
+
items.push(assistantMessageItem(assistantTextBlocks, timestamp));
|
|
879
|
+
assistantTextBlocks.length = 0;
|
|
880
|
+
}
|
|
881
|
+
if (block.kind === "thinking") {
|
|
882
|
+
items.push({
|
|
883
|
+
kind: "claude_thinking",
|
|
884
|
+
text: block.text,
|
|
885
|
+
...(timestamp ? { timestamp } : {})
|
|
886
|
+
});
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
if (block.kind === "tool_use") {
|
|
890
|
+
const taskItem = taskChecklistFromToolUse(block, timestamp);
|
|
891
|
+
if (taskItem) {
|
|
892
|
+
items.push(taskItem);
|
|
893
|
+
}
|
|
894
|
+
else {
|
|
895
|
+
const toolCategory = toolCategoryForClaudeName(block.name);
|
|
896
|
+
if (block.toolUseId) {
|
|
897
|
+
toolCallsById.set(block.toolUseId, { name: block.name, timelineIndex: timeline.length + items.length, toolCategory });
|
|
898
|
+
}
|
|
899
|
+
items.push({
|
|
900
|
+
input: block.input,
|
|
901
|
+
kind: "claude_tool_use",
|
|
902
|
+
name: block.name,
|
|
903
|
+
toolCategory,
|
|
904
|
+
...(block.toolUseId ? { toolUseId: block.toolUseId } : {}),
|
|
905
|
+
...(timestamp ? { timestamp } : {})
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
if (block.kind === "tool_result") {
|
|
911
|
+
const call = block.toolUseId ? toolCallsById.get(block.toolUseId) : undefined;
|
|
912
|
+
const toolResult = {
|
|
913
|
+
isError: block.isError,
|
|
914
|
+
kind: "claude_tool_result",
|
|
915
|
+
...(call?.name ? { name: call.name } : {}),
|
|
916
|
+
output: block.text,
|
|
917
|
+
toolCategory: call?.toolCategory ?? "generic",
|
|
918
|
+
...(block.toolUseId ? { toolUseId: block.toolUseId } : {}),
|
|
919
|
+
...(timestamp ? { timestamp } : {})
|
|
920
|
+
};
|
|
921
|
+
if (call?.timelineIndex !== undefined) {
|
|
922
|
+
const toolUse = timeline[call.timelineIndex];
|
|
923
|
+
if (toolUse?.kind === "claude_tool_use") {
|
|
924
|
+
timeline[call.timelineIndex] = {
|
|
925
|
+
kind: "claude_tool_exchange",
|
|
926
|
+
result: {
|
|
927
|
+
isError: block.isError,
|
|
928
|
+
output: block.text,
|
|
929
|
+
...(timestamp ? { timestamp } : {})
|
|
930
|
+
},
|
|
931
|
+
toolUse: {
|
|
932
|
+
input: toolUse.input,
|
|
933
|
+
name: toolUse.name,
|
|
934
|
+
toolCategory: toolUse.toolCategory,
|
|
935
|
+
...(toolUse.timestamp ? { timestamp: toolUse.timestamp } : {})
|
|
936
|
+
},
|
|
937
|
+
...(block.toolUseId ? { toolUseId: block.toolUseId } : {}),
|
|
938
|
+
...(toolUse.timestamp ? { timestamp: toolUse.timestamp } : timestamp ? { timestamp } : {})
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
else {
|
|
942
|
+
items.push(toolResult);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
else {
|
|
946
|
+
items.push(toolResult);
|
|
947
|
+
}
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
if (block.kind === "text") {
|
|
951
|
+
items.push({
|
|
952
|
+
kind: "claude_user_message",
|
|
953
|
+
text: block.text,
|
|
954
|
+
...(timestamp ? { timestamp } : {})
|
|
955
|
+
});
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
items.push({
|
|
959
|
+
kind: "claude_attachment",
|
|
960
|
+
label: block.blockType,
|
|
961
|
+
text: block.text,
|
|
962
|
+
...(timestamp ? { timestamp } : {})
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
if (assistantTextBlocks.length > 0) {
|
|
966
|
+
items.push(assistantMessageItem(assistantTextBlocks, timestamp));
|
|
967
|
+
}
|
|
968
|
+
return items;
|
|
969
|
+
}
|
|
970
|
+
function assistantMessageItem(blocks, timestamp) {
|
|
971
|
+
return {
|
|
972
|
+
blocks: [...blocks],
|
|
973
|
+
kind: "claude_assistant_message",
|
|
974
|
+
text: blocks.map((block) => blockText(block)).join("\n\n"),
|
|
975
|
+
...(timestamp ? { timestamp } : {})
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
function timelineItemFromMetadataEntry(entry) {
|
|
979
|
+
const timestamp = typeof entry.timestamp === "string" ? entry.timestamp : undefined;
|
|
980
|
+
if (entry.type === "mode") {
|
|
981
|
+
return sessionEvent("Mode", typeof entry.mode === "string" ? entry.mode : safeJson(entry), timestamp);
|
|
982
|
+
}
|
|
983
|
+
if (entry.type === "permission-mode") {
|
|
984
|
+
return sessionEvent("Permissions", typeof entry.permissionMode === "string" ? entry.permissionMode : safeJson(entry), timestamp, "warning");
|
|
985
|
+
}
|
|
986
|
+
if (entry.type === "ai-title") {
|
|
987
|
+
return sessionEvent("Title", typeof entry.aiTitle === "string" ? entry.aiTitle : safeJson(entry), timestamp);
|
|
988
|
+
}
|
|
989
|
+
if (entry.type === "agent-name") {
|
|
990
|
+
return sessionEvent("Agent", typeof entry.agentName === "string" ? entry.agentName : safeJson(entry), timestamp);
|
|
991
|
+
}
|
|
992
|
+
if (entry.type === "last-prompt") {
|
|
993
|
+
return sessionEvent("Last prompt", typeof entry.lastPrompt === "string" ? entry.lastPrompt : safeJson(entry), timestamp);
|
|
994
|
+
}
|
|
995
|
+
if (entry.type === "queue-operation") {
|
|
996
|
+
return {
|
|
997
|
+
kind: "claude_queued_message",
|
|
998
|
+
operation: typeof entry.operation === "string" ? entry.operation : "queued",
|
|
999
|
+
text: typeof entry.content === "string" ? entry.content : safeJson(entry),
|
|
1000
|
+
...(timestamp ? { timestamp } : {})
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
if (entry.type === "attachment") {
|
|
1004
|
+
const attachment = entry.attachment && typeof entry.attachment === "object" ? entry.attachment : entry;
|
|
1005
|
+
return timelineItemFromAttachment(attachment, timestamp);
|
|
1006
|
+
}
|
|
1007
|
+
if (entry.type === "file-history-snapshot") {
|
|
1008
|
+
return {
|
|
1009
|
+
kind: "claude_file_snapshot",
|
|
1010
|
+
label: entry.isSnapshotUpdate === true ? "File snapshot updated" : "File snapshot",
|
|
1011
|
+
text: typeof entry.messageId === "string" ? entry.messageId : safeJson(entry),
|
|
1012
|
+
...(timestamp ? { timestamp } : {})
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
if (entry.type === "pr-link" && typeof entry.prUrl === "string") {
|
|
1016
|
+
return {
|
|
1017
|
+
kind: "claude_pr_link",
|
|
1018
|
+
...(typeof entry.prRepository === "string" ? { repository: entry.prRepository } : {}),
|
|
1019
|
+
title: typeof entry.prNumber === "number" ? `Pull request #${entry.prNumber}` : "Pull request",
|
|
1020
|
+
url: entry.prUrl,
|
|
1021
|
+
...(timestamp ? { timestamp } : {})
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
if (entry.type === "system" && entry.subtype === "turn_duration") {
|
|
1025
|
+
return sessionEvent("Turn duration", typeof entry.durationMs === "number" ? `${Math.round(entry.durationMs / 1000)}s` : safeJson(entry), timestamp, "success");
|
|
1026
|
+
}
|
|
1027
|
+
if (entry.type === "system" && entry.subtype === "away_summary") {
|
|
1028
|
+
return sessionEvent("Recap", textContentFromEntry(entry), timestamp);
|
|
1029
|
+
}
|
|
1030
|
+
if (entry.type === "system" && entry.subtype === "scheduled_task_fire") {
|
|
1031
|
+
return sessionEvent("Scheduled resume", textContentFromEntry(entry), timestamp, "warning");
|
|
1032
|
+
}
|
|
1033
|
+
if (entry.type === "system" && entry.subtype === "compact_boundary") {
|
|
1034
|
+
return sessionEvent("Compacted", textContentFromEntry(entry), timestamp);
|
|
1035
|
+
}
|
|
1036
|
+
if (entry.type === "system" && entry.subtype === "api_error") {
|
|
1037
|
+
return sessionEvent("API error", summarizeApiError(entry), timestamp, "warning");
|
|
1038
|
+
}
|
|
1039
|
+
if (entry.type === "system" && entry.subtype === "local_command") {
|
|
1040
|
+
return sessionEvent("Local command", textContentFromEntry(entry), timestamp);
|
|
1041
|
+
}
|
|
1042
|
+
if (entry.type === "system" && entry.subtype === "stop_hook_summary") {
|
|
1043
|
+
return sessionEvent("Stop hook", summarizeStopHook(entry), timestamp);
|
|
1044
|
+
}
|
|
1045
|
+
if (entry.type === "started") {
|
|
1046
|
+
return sessionEvent("Agent started", summarizeAgentEvent(entry), timestamp, "success");
|
|
1047
|
+
}
|
|
1048
|
+
if (entry.type === "result") {
|
|
1049
|
+
return sessionEvent("Agent result", summarizeAgentEvent(entry), timestamp, "success");
|
|
1050
|
+
}
|
|
1051
|
+
return undefined;
|
|
1052
|
+
}
|
|
1053
|
+
function sessionEvent(label, text, timestamp, tone) {
|
|
1054
|
+
return {
|
|
1055
|
+
kind: "claude_system_event",
|
|
1056
|
+
label,
|
|
1057
|
+
text,
|
|
1058
|
+
...(tone ? { tone } : {}),
|
|
1059
|
+
...(timestamp ? { timestamp } : {})
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
function claudeInterruptionEvent(text, timestamp) {
|
|
1063
|
+
const trimmed = text.trim();
|
|
1064
|
+
if (trimmed === "[Request interrupted by user]") {
|
|
1065
|
+
return sessionEvent("Interrupted", "Request interrupted by user.", timestamp, "warning");
|
|
1066
|
+
}
|
|
1067
|
+
if (trimmed === "[Request interrupted by user for tool use]") {
|
|
1068
|
+
return sessionEvent("Interrupted", "Request interrupted by user for tool use.", timestamp, "warning");
|
|
1069
|
+
}
|
|
1070
|
+
return undefined;
|
|
1071
|
+
}
|
|
1072
|
+
function timelineItemFromAttachment(attachment, timestamp) {
|
|
1073
|
+
const attachmentType = typeof attachment.type === "string" ? attachment.type : "";
|
|
1074
|
+
if (attachmentType === "hook_non_blocking_error") {
|
|
1075
|
+
return sessionEvent("Hook warning", summarizeHookAttachment(attachment), timestamp, "warning");
|
|
1076
|
+
}
|
|
1077
|
+
if (attachmentType === "hook_success") {
|
|
1078
|
+
return sessionEvent("Hook success", summarizeHookAttachment(attachment), timestamp, "success");
|
|
1079
|
+
}
|
|
1080
|
+
if (attachmentType === "task_reminder" || attachmentType === "task_status") {
|
|
1081
|
+
return sessionEvent("Task reminder", summarizeAttachment(attachment), timestamp, "warning");
|
|
1082
|
+
}
|
|
1083
|
+
if (attachmentType === "deferred_tools_delta") {
|
|
1084
|
+
return sessionEvent("Tools updated", summarizeAttachment(attachment), timestamp);
|
|
1085
|
+
}
|
|
1086
|
+
if (attachmentType === "command_permissions") {
|
|
1087
|
+
return sessionEvent("Command permissions", summarizeAttachment(attachment), timestamp);
|
|
1088
|
+
}
|
|
1089
|
+
if (attachmentType === "skill_listing" || attachmentType === "invoked_skills" || attachmentType === "dynamic_skill") {
|
|
1090
|
+
return sessionEvent("Skills", summarizeAttachment(attachment), timestamp);
|
|
1091
|
+
}
|
|
1092
|
+
if (attachmentType === "nested_memory") {
|
|
1093
|
+
return sessionEvent("Memory", summarizeAttachment(attachment), timestamp);
|
|
1094
|
+
}
|
|
1095
|
+
if (attachmentType === "agent_listing_delta") {
|
|
1096
|
+
return sessionEvent("Agents", summarizeAttachment(attachment), timestamp);
|
|
1097
|
+
}
|
|
1098
|
+
if (attachmentType === "date_change") {
|
|
1099
|
+
return sessionEvent("Date changed", summarizeAttachment(attachment), timestamp);
|
|
1100
|
+
}
|
|
1101
|
+
if (attachmentType === "workflow_keyword_request") {
|
|
1102
|
+
return sessionEvent("Workflow request", summarizeAttachment(attachment), timestamp, "warning");
|
|
1103
|
+
}
|
|
1104
|
+
if (attachmentType === "queued_command") {
|
|
1105
|
+
return {
|
|
1106
|
+
kind: "claude_queued_message",
|
|
1107
|
+
operation: "queued",
|
|
1108
|
+
text: summarizeAttachment(attachment),
|
|
1109
|
+
...(timestamp ? { timestamp } : {})
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
if (attachmentType === "file" || attachmentType === "edited_text_file" || attachmentType === "compact_file_reference" || attachmentType === "plan_file_reference") {
|
|
1113
|
+
return {
|
|
1114
|
+
kind: "claude_file_snapshot",
|
|
1115
|
+
label: attachmentType === "edited_text_file" ? "Edited file" : "File reference",
|
|
1116
|
+
text: summarizeAttachment(attachment),
|
|
1117
|
+
...(timestamp ? { timestamp } : {})
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
if (attachmentType === "plan_mode" || attachmentType === "plan_mode_exit" || attachmentType === "plan_mode_reentry") {
|
|
1121
|
+
return sessionEvent("Plan mode", summarizeAttachment(attachment), timestamp);
|
|
1122
|
+
}
|
|
1123
|
+
return {
|
|
1124
|
+
kind: "claude_attachment",
|
|
1125
|
+
label: attachmentType || "Attachment",
|
|
1126
|
+
text: summarizeAttachment(attachment),
|
|
1127
|
+
...(timestamp ? { timestamp } : {})
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
function taskChecklistFromToolUse(block, timestamp) {
|
|
1131
|
+
if (block.name !== "TodoWrite")
|
|
1132
|
+
return undefined;
|
|
1133
|
+
let parsed;
|
|
1134
|
+
try {
|
|
1135
|
+
parsed = JSON.parse(block.input);
|
|
1136
|
+
}
|
|
1137
|
+
catch {
|
|
1138
|
+
return undefined;
|
|
1139
|
+
}
|
|
1140
|
+
const todos = parsed && typeof parsed === "object" && Array.isArray(parsed.todos)
|
|
1141
|
+
? parsed.todos
|
|
1142
|
+
: [];
|
|
1143
|
+
if (todos.length === 0)
|
|
1144
|
+
return undefined;
|
|
1145
|
+
return {
|
|
1146
|
+
items: todos.map((todo) => {
|
|
1147
|
+
if (!todo || typeof todo !== "object")
|
|
1148
|
+
return { text: safeJson(todo) };
|
|
1149
|
+
const record = todo;
|
|
1150
|
+
return {
|
|
1151
|
+
...(typeof record.status === "string" ? { status: record.status } : {}),
|
|
1152
|
+
text: typeof record.content === "string" ? record.content : safeJson(record)
|
|
1153
|
+
};
|
|
1154
|
+
}),
|
|
1155
|
+
kind: "claude_todo",
|
|
1156
|
+
title: "Task checklist",
|
|
1157
|
+
...(timestamp ? { timestamp } : {})
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
function toolCategoryForClaudeName(name) {
|
|
1161
|
+
if (name === "Bash" || name === "bash")
|
|
1162
|
+
return "shell";
|
|
1163
|
+
if (name === "Read")
|
|
1164
|
+
return "file_read";
|
|
1165
|
+
if (name === "Edit" || name === "MultiEdit" || name === "Write" || name === "NotebookEdit")
|
|
1166
|
+
return "file_edit";
|
|
1167
|
+
if (name === "Grep" || name === "Glob" || name === "LS")
|
|
1168
|
+
return "search";
|
|
1169
|
+
if (name === "WebFetch" || name === "WebSearch")
|
|
1170
|
+
return "web";
|
|
1171
|
+
if (name === "Agent" || name === "Task")
|
|
1172
|
+
return "subagent";
|
|
1173
|
+
if (name === "AskUserQuestion")
|
|
1174
|
+
return "human";
|
|
1175
|
+
if (name === "CronCreate" || name === "CronDelete" || name === "ScheduleWakeup")
|
|
1176
|
+
return "scheduler";
|
|
1177
|
+
if (name === "EnterPlanMode" || name === "ExitPlanMode")
|
|
1178
|
+
return "planning";
|
|
1179
|
+
if (name === "Monitor")
|
|
1180
|
+
return "monitor";
|
|
1181
|
+
if (name === "SendMessage")
|
|
1182
|
+
return "workflow";
|
|
1183
|
+
if (name === "Skill" || name === "ToolSearch")
|
|
1184
|
+
return "discovery";
|
|
1185
|
+
if (name === "StructuredOutput")
|
|
1186
|
+
return "structured";
|
|
1187
|
+
if (name === "TaskCreate" || name === "TaskGet" || name === "TaskList" || name === "TaskOutput" || name === "TaskStop" || name === "TaskUpdate" || name === "TodoWrite")
|
|
1188
|
+
return "task";
|
|
1189
|
+
if (name === "Workflow")
|
|
1190
|
+
return "workflow";
|
|
1191
|
+
if (name.startsWith("mcp__playwright__browser_"))
|
|
1192
|
+
return "browser";
|
|
1193
|
+
if (name.startsWith("mcp__"))
|
|
1194
|
+
return "mcp";
|
|
1195
|
+
return "generic";
|
|
1196
|
+
}
|
|
1197
|
+
function blocksFromContent(content) {
|
|
1198
|
+
if (typeof content === "string") {
|
|
1199
|
+
return content.trim() ? [{ kind: "text", text: content }] : [];
|
|
1200
|
+
}
|
|
1201
|
+
if (!Array.isArray(content))
|
|
1202
|
+
return [];
|
|
1203
|
+
return content.flatMap((block) => {
|
|
1204
|
+
if (!block || typeof block !== "object")
|
|
1205
|
+
return [];
|
|
1206
|
+
const record = block;
|
|
1207
|
+
const type = typeof record.type === "string" ? record.type : "unknown";
|
|
1208
|
+
if (type === "text") {
|
|
1209
|
+
return typeof record.text === "string" && record.text.trim()
|
|
1210
|
+
? [{ kind: "text", text: record.text }]
|
|
1211
|
+
: [];
|
|
1212
|
+
}
|
|
1213
|
+
if (type === "thinking") {
|
|
1214
|
+
return typeof record.thinking === "string" && record.thinking.trim()
|
|
1215
|
+
? [{ kind: "thinking", text: record.thinking }]
|
|
1216
|
+
: [];
|
|
1217
|
+
}
|
|
1218
|
+
if (type === "tool_use") {
|
|
1219
|
+
return [{
|
|
1220
|
+
input: stringifyToolInput(record.input),
|
|
1221
|
+
kind: "tool_use",
|
|
1222
|
+
name: typeof record.name === "string" ? record.name : "Tool",
|
|
1223
|
+
...(typeof record.id === "string" ? { toolUseId: record.id } : {})
|
|
1224
|
+
}];
|
|
1225
|
+
}
|
|
1226
|
+
if (type === "tool_result") {
|
|
1227
|
+
return [{
|
|
1228
|
+
isError: record.is_error === true,
|
|
1229
|
+
kind: "tool_result",
|
|
1230
|
+
text: toolResultText(record.content),
|
|
1231
|
+
...(typeof record.tool_use_id === "string" ? { toolUseId: record.tool_use_id } : {})
|
|
1232
|
+
}];
|
|
1233
|
+
}
|
|
1234
|
+
return [{
|
|
1235
|
+
blockType: type,
|
|
1236
|
+
kind: "unknown",
|
|
1237
|
+
text: safeJson(record)
|
|
1238
|
+
}];
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
function blockText(block) {
|
|
1242
|
+
if (block.kind === "tool_use")
|
|
1243
|
+
return `${block.name}\n${block.input}`;
|
|
1244
|
+
if (block.kind === "tool_result")
|
|
1245
|
+
return block.text;
|
|
1246
|
+
return block.text;
|
|
1247
|
+
}
|
|
1248
|
+
function stringifyToolInput(input) {
|
|
1249
|
+
if (!input || typeof input !== "object")
|
|
1250
|
+
return "";
|
|
1251
|
+
const record = input;
|
|
1252
|
+
if (typeof record.command === "string") {
|
|
1253
|
+
return [
|
|
1254
|
+
typeof record.description === "string" ? record.description : undefined,
|
|
1255
|
+
record.command
|
|
1256
|
+
].filter((part) => Boolean(part)).join("\n");
|
|
1257
|
+
}
|
|
1258
|
+
return safeJson(input);
|
|
1259
|
+
}
|
|
1260
|
+
function toolResultText(content) {
|
|
1261
|
+
if (typeof content === "string")
|
|
1262
|
+
return content;
|
|
1263
|
+
if (Array.isArray(content)) {
|
|
1264
|
+
return content.map(summarizeToolResultPart).join("\n");
|
|
1265
|
+
}
|
|
1266
|
+
return summarizeToolResultPart(content);
|
|
1267
|
+
}
|
|
1268
|
+
function summarizeAttachment(attachment) {
|
|
1269
|
+
if (Array.isArray(attachment.addedNames)) {
|
|
1270
|
+
return `Added tools: ${attachment.addedNames.filter((name) => typeof name === "string").slice(0, 12).join(", ")}`;
|
|
1271
|
+
}
|
|
1272
|
+
if (Array.isArray(attachment.allowedTools)) {
|
|
1273
|
+
const tools = attachment.allowedTools.filter((name) => typeof name === "string");
|
|
1274
|
+
return tools.length > 0 ? `Allowed tools: ${tools.slice(0, 12).join(", ")}` : "No command tools allowed.";
|
|
1275
|
+
}
|
|
1276
|
+
if (typeof attachment.filePath === "string")
|
|
1277
|
+
return attachment.filePath;
|
|
1278
|
+
if (typeof attachment.path === "string")
|
|
1279
|
+
return attachment.path;
|
|
1280
|
+
if (typeof attachment.filename === "string")
|
|
1281
|
+
return attachment.filename;
|
|
1282
|
+
if (typeof attachment.content === "string")
|
|
1283
|
+
return attachment.content;
|
|
1284
|
+
if (Array.isArray(attachment.content))
|
|
1285
|
+
return attachment.content.length > 0 ? safeJson(attachment.content) : "No active items.";
|
|
1286
|
+
if (typeof attachment.memoryType === "string")
|
|
1287
|
+
return attachment.memoryType;
|
|
1288
|
+
if (typeof attachment.date === "string")
|
|
1289
|
+
return attachment.date;
|
|
1290
|
+
if (typeof attachment.keyword === "string")
|
|
1291
|
+
return attachment.keyword;
|
|
1292
|
+
if (typeof attachment.text === "string")
|
|
1293
|
+
return attachment.text;
|
|
1294
|
+
if (typeof attachment.name === "string")
|
|
1295
|
+
return attachment.name;
|
|
1296
|
+
return safeJson(attachment);
|
|
1297
|
+
}
|
|
1298
|
+
function summarizeHookAttachment(attachment) {
|
|
1299
|
+
return [
|
|
1300
|
+
typeof attachment.hookName === "string" ? attachment.hookName : undefined,
|
|
1301
|
+
typeof attachment.command === "string" ? attachment.command : undefined,
|
|
1302
|
+
typeof attachment.stderr === "string" && attachment.stderr.trim() ? attachment.stderr : undefined,
|
|
1303
|
+
typeof attachment.stdout === "string" && attachment.stdout.trim() ? attachment.stdout : undefined,
|
|
1304
|
+
typeof attachment.exitCode === "number" ? `exit ${attachment.exitCode}` : undefined
|
|
1305
|
+
].filter((part) => Boolean(part)).join("\n") || summarizeAttachment(attachment);
|
|
1306
|
+
}
|
|
1307
|
+
function textContentFromEntry(entry) {
|
|
1308
|
+
return typeof entry.content === "string" && entry.content.trim() ? entry.content : safeJson(entry);
|
|
1309
|
+
}
|
|
1310
|
+
function summarizeApiError(entry) {
|
|
1311
|
+
const retryInMs = typeof entry.retryInMs === "number" ? ` Retry in ${Math.round(entry.retryInMs)}ms.` : "";
|
|
1312
|
+
const retryAttempt = typeof entry.retryAttempt === "number" && typeof entry.maxRetries === "number"
|
|
1313
|
+
? ` Attempt ${entry.retryAttempt}/${entry.maxRetries}.`
|
|
1314
|
+
: "";
|
|
1315
|
+
const cause = entry.cause && typeof entry.cause === "object" ? entry.cause : undefined;
|
|
1316
|
+
const code = typeof cause?.code === "string" ? cause.code : undefined;
|
|
1317
|
+
const path = typeof cause?.path === "string" ? cause.path : undefined;
|
|
1318
|
+
return [code, path].filter(Boolean).join(" ") + retryAttempt + retryInMs || safeJson(entry);
|
|
1319
|
+
}
|
|
1320
|
+
function summarizeStopHook(entry) {
|
|
1321
|
+
const hookCount = typeof entry.hookCount === "number" ? `${entry.hookCount} hook${entry.hookCount === 1 ? "" : "s"}` : "Stop hook";
|
|
1322
|
+
const prevented = entry.preventedContinuation === true ? "prevented continuation" : "completed";
|
|
1323
|
+
const errors = Array.isArray(entry.hookErrors) && entry.hookErrors.length > 0 ? ` with ${entry.hookErrors.length} error${entry.hookErrors.length === 1 ? "" : "s"}` : "";
|
|
1324
|
+
return `${hookCount} ${prevented}${errors}.`;
|
|
1325
|
+
}
|
|
1326
|
+
function summarizeAgentEvent(entry) {
|
|
1327
|
+
const parts = [
|
|
1328
|
+
typeof entry.agentId === "string" ? `agent ${entry.agentId}` : undefined,
|
|
1329
|
+
typeof entry.skill === "string" ? `skill ${entry.skill}` : undefined,
|
|
1330
|
+
typeof entry.runId === "string" ? `run ${entry.runId}` : undefined,
|
|
1331
|
+
typeof entry.key === "string" ? entry.key : undefined
|
|
1332
|
+
].filter((part) => Boolean(part));
|
|
1333
|
+
if (entry.result !== undefined) {
|
|
1334
|
+
return `${parts.join(" · ") || "agent result"}\n${safeJson(entry.result)}`;
|
|
1335
|
+
}
|
|
1336
|
+
if (typeof entry.context === "string") {
|
|
1337
|
+
return `${parts.join(" · ") || "agent started"}\n${entry.context}`;
|
|
1338
|
+
}
|
|
1339
|
+
return parts.join(" · ") || safeJson(entry);
|
|
1340
|
+
}
|
|
1341
|
+
function findCodexTranscriptById(root, sessionId) {
|
|
1342
|
+
const candidates = Array.from(walkJsonl(root)).filter((path) => path.includes(sessionId));
|
|
1343
|
+
if (candidates.length > 0)
|
|
1344
|
+
return newestFile(candidates);
|
|
1345
|
+
for (const path of walkJsonl(root)) {
|
|
1346
|
+
let firstLine = "";
|
|
1347
|
+
try {
|
|
1348
|
+
firstLine = readFirstLine(path);
|
|
1349
|
+
}
|
|
1350
|
+
catch {
|
|
1351
|
+
continue;
|
|
1352
|
+
}
|
|
1353
|
+
if (!firstLine.trim())
|
|
1354
|
+
continue;
|
|
1355
|
+
try {
|
|
1356
|
+
const entry = JSON.parse(firstLine);
|
|
1357
|
+
if (entry.payload?.id === sessionId)
|
|
1358
|
+
return path;
|
|
1359
|
+
}
|
|
1360
|
+
catch {
|
|
1361
|
+
continue;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
return undefined;
|
|
1365
|
+
}
|
|
1366
|
+
function findClaudeTranscriptById(root, sessionId) {
|
|
1367
|
+
const filename = `${sessionId}.jsonl`;
|
|
1368
|
+
const candidates = Array.from(walkJsonl(root)).filter((path) => basename(path) === filename);
|
|
1369
|
+
if (candidates.length === 1 && candidates[0]) {
|
|
1370
|
+
return candidates[0];
|
|
1371
|
+
}
|
|
1372
|
+
if (candidates.length > 1) {
|
|
1373
|
+
throw new Error(`Claude session transcript lookup for ${sessionId} is ambiguous: ${candidates.join(", ")}`);
|
|
1374
|
+
}
|
|
1375
|
+
return join(root, "__missing__", filename);
|
|
1376
|
+
}
|
|
1377
|
+
// Session header lines are small; reading a bounded prefix avoids pulling
|
|
1378
|
+
// entire multi-megabyte transcripts into memory during id lookups.
|
|
1379
|
+
function readFirstLine(path, maxBytes = 16_384) {
|
|
1380
|
+
const fd = openSync(path, "r");
|
|
1381
|
+
try {
|
|
1382
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
1383
|
+
const bytesRead = readSync(fd, buffer, 0, maxBytes, 0);
|
|
1384
|
+
const text = buffer.toString("utf8", 0, bytesRead);
|
|
1385
|
+
const newlineIndex = text.indexOf("\n");
|
|
1386
|
+
return newlineIndex === -1 ? text : text.slice(0, newlineIndex);
|
|
1387
|
+
}
|
|
1388
|
+
finally {
|
|
1389
|
+
closeSync(fd);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
function* walkJsonl(root) {
|
|
1393
|
+
let entries;
|
|
1394
|
+
try {
|
|
1395
|
+
entries = readdirSync(root);
|
|
1396
|
+
}
|
|
1397
|
+
catch {
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
for (const entry of entries) {
|
|
1401
|
+
const path = join(root, entry);
|
|
1402
|
+
let stat;
|
|
1403
|
+
try {
|
|
1404
|
+
stat = statSync(path);
|
|
1405
|
+
}
|
|
1406
|
+
catch {
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
if (stat.isDirectory()) {
|
|
1410
|
+
yield* walkJsonl(path);
|
|
1411
|
+
}
|
|
1412
|
+
else if (path.endsWith(".jsonl")) {
|
|
1413
|
+
yield path;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
function newestFile(paths) {
|
|
1418
|
+
return paths
|
|
1419
|
+
.map((path) => {
|
|
1420
|
+
try {
|
|
1421
|
+
return { mtime: statSync(path).mtimeMs, path };
|
|
1422
|
+
}
|
|
1423
|
+
catch {
|
|
1424
|
+
return undefined;
|
|
1425
|
+
}
|
|
1426
|
+
})
|
|
1427
|
+
.filter((item) => Boolean(item))
|
|
1428
|
+
.sort((a, b) => b.mtime - a.mtime)[0]?.path;
|
|
1429
|
+
}
|
|
1430
|
+
function timestampFromEntry(entry) {
|
|
1431
|
+
if (typeof entry.timestamp === "string")
|
|
1432
|
+
return entry.timestamp;
|
|
1433
|
+
const payload = entry.payload && typeof entry.payload === "object" ? entry.payload : undefined;
|
|
1434
|
+
return timestampFromPayload(payload, "timestamp")
|
|
1435
|
+
?? timestampFromPayload(payload, "started_at")
|
|
1436
|
+
?? timestampFromPayload(payload, "completed_at");
|
|
1437
|
+
}
|
|
1438
|
+
function timestampFromPayload(payload, key) {
|
|
1439
|
+
const value = payload?.[key];
|
|
1440
|
+
if (typeof value === "string")
|
|
1441
|
+
return value;
|
|
1442
|
+
if (typeof value === "number")
|
|
1443
|
+
return new Date(value > 10_000_000_000 ? value : value * 1000).toISOString();
|
|
1444
|
+
return undefined;
|
|
1445
|
+
}
|
|
1446
|
+
function codexContentText(content) {
|
|
1447
|
+
if (typeof content === "string")
|
|
1448
|
+
return content;
|
|
1449
|
+
if (!Array.isArray(content))
|
|
1450
|
+
return "";
|
|
1451
|
+
return content.map((item) => {
|
|
1452
|
+
if (typeof item === "string")
|
|
1453
|
+
return item;
|
|
1454
|
+
if (item && typeof item === "object") {
|
|
1455
|
+
const record = item;
|
|
1456
|
+
if (typeof record.text === "string")
|
|
1457
|
+
return record.text;
|
|
1458
|
+
}
|
|
1459
|
+
return "";
|
|
1460
|
+
}).filter(Boolean).join("\n\n");
|
|
1461
|
+
}
|
|
1462
|
+
function codexReasoningSummary(summary) {
|
|
1463
|
+
if (typeof summary === "string" && summary.trim())
|
|
1464
|
+
return summary;
|
|
1465
|
+
if (!Array.isArray(summary))
|
|
1466
|
+
return undefined;
|
|
1467
|
+
const text = summary.map((item) => {
|
|
1468
|
+
if (typeof item === "string")
|
|
1469
|
+
return item;
|
|
1470
|
+
if (item && typeof item === "object" && typeof item.text === "string") {
|
|
1471
|
+
return item.text;
|
|
1472
|
+
}
|
|
1473
|
+
return "";
|
|
1474
|
+
}).filter(Boolean).join("\n");
|
|
1475
|
+
return text || undefined;
|
|
1476
|
+
}
|
|
1477
|
+
function codexToolName(payload) {
|
|
1478
|
+
if (typeof payload.name === "string")
|
|
1479
|
+
return payload.name;
|
|
1480
|
+
if (payload.type === "web_search_call")
|
|
1481
|
+
return "web_search";
|
|
1482
|
+
if (payload.type === "tool_search_call")
|
|
1483
|
+
return "tool_search";
|
|
1484
|
+
return "tool";
|
|
1485
|
+
}
|
|
1486
|
+
function codexToolArguments(payload) {
|
|
1487
|
+
if (typeof payload.arguments === "string")
|
|
1488
|
+
return prettyJsonString(payload.arguments);
|
|
1489
|
+
if (typeof payload.input === "string")
|
|
1490
|
+
return payload.input;
|
|
1491
|
+
if (payload.arguments !== undefined)
|
|
1492
|
+
return safeJson(payload.arguments);
|
|
1493
|
+
if (payload.action !== undefined)
|
|
1494
|
+
return safeJson(payload.action);
|
|
1495
|
+
return safeJson(payload);
|
|
1496
|
+
}
|
|
1497
|
+
function codexToolOutput(payload) {
|
|
1498
|
+
if (typeof payload.output === "string")
|
|
1499
|
+
return summarizeCodexOutputString(payload.output);
|
|
1500
|
+
if (payload.output !== undefined)
|
|
1501
|
+
return summarizeStructuredToolOutput(payload.output);
|
|
1502
|
+
if (payload.tools !== undefined)
|
|
1503
|
+
return summarizeCodexTools(payload.tools);
|
|
1504
|
+
return safeJson(payload);
|
|
1505
|
+
}
|
|
1506
|
+
function summarizeStructuredToolOutput(output) {
|
|
1507
|
+
if (typeof output === "string")
|
|
1508
|
+
return output;
|
|
1509
|
+
if (Array.isArray(output))
|
|
1510
|
+
return output.map(summarizeToolResultPart).join("\n");
|
|
1511
|
+
return safeJson(output);
|
|
1512
|
+
}
|
|
1513
|
+
function summarizeCodexOutputString(output) {
|
|
1514
|
+
if (!output.trim())
|
|
1515
|
+
return output;
|
|
1516
|
+
try {
|
|
1517
|
+
const parsed = JSON.parse(output);
|
|
1518
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1519
|
+
const record = parsed;
|
|
1520
|
+
if (typeof record.output === "string")
|
|
1521
|
+
return record.output;
|
|
1522
|
+
if (record.metadata !== undefined)
|
|
1523
|
+
return summarizeKeyValues(record, ["status", "exit_code", "duration_seconds"]);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
catch {
|
|
1527
|
+
// Plain text tool output is the common case.
|
|
1528
|
+
}
|
|
1529
|
+
return output;
|
|
1530
|
+
}
|
|
1531
|
+
function summarizeCodexTools(tools) {
|
|
1532
|
+
if (!Array.isArray(tools))
|
|
1533
|
+
return safeJson(tools);
|
|
1534
|
+
const names = tools.flatMap((tool) => {
|
|
1535
|
+
if (!tool || typeof tool !== "object")
|
|
1536
|
+
return [];
|
|
1537
|
+
const record = tool;
|
|
1538
|
+
const nested = Array.isArray(record.tools)
|
|
1539
|
+
? record.tools.flatMap((child) => {
|
|
1540
|
+
if (!child || typeof child !== "object")
|
|
1541
|
+
return [];
|
|
1542
|
+
const childName = child.name;
|
|
1543
|
+
return typeof childName === "string" ? [childName] : [];
|
|
1544
|
+
})
|
|
1545
|
+
: [];
|
|
1546
|
+
return typeof record.name === "string" ? [record.name, ...nested] : nested;
|
|
1547
|
+
});
|
|
1548
|
+
return names.length > 0 ? `Tools found: ${names.slice(0, 16).join(", ")}` : `${tools.length} tool result${tools.length === 1 ? "" : "s"}.`;
|
|
1549
|
+
}
|
|
1550
|
+
function summarizeToolResultPart(item) {
|
|
1551
|
+
if (typeof item === "string")
|
|
1552
|
+
return item;
|
|
1553
|
+
if (!item || typeof item !== "object")
|
|
1554
|
+
return safeJson(item);
|
|
1555
|
+
const record = item;
|
|
1556
|
+
if (typeof record.text === "string")
|
|
1557
|
+
return record.text;
|
|
1558
|
+
const type = typeof record.type === "string" ? record.type : "";
|
|
1559
|
+
if (type === "image" || type === "input_image") {
|
|
1560
|
+
const source = record.source && typeof record.source === "object" ? record.source : undefined;
|
|
1561
|
+
const mediaType = typeof source?.media_type === "string"
|
|
1562
|
+
? source.media_type
|
|
1563
|
+
: typeof record.image_url === "string"
|
|
1564
|
+
? record.image_url.match(/^data:([^;,]+)/)?.[1]
|
|
1565
|
+
: undefined;
|
|
1566
|
+
return mediaType ? `Image result (${mediaType})` : "Image result";
|
|
1567
|
+
}
|
|
1568
|
+
if (type === "tool_reference") {
|
|
1569
|
+
const toolName = typeof record.tool_name === "string" ? record.tool_name : undefined;
|
|
1570
|
+
return toolName ? `Tool reference: ${toolName}` : "Tool reference";
|
|
1571
|
+
}
|
|
1572
|
+
return safeJson(item);
|
|
1573
|
+
}
|
|
1574
|
+
function prettyJsonString(value) {
|
|
1575
|
+
try {
|
|
1576
|
+
return JSON.stringify(JSON.parse(value), null, 2);
|
|
1577
|
+
}
|
|
1578
|
+
catch {
|
|
1579
|
+
return value;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
function toolCategoryForCodexName(name) {
|
|
1583
|
+
if (name === "exec_command" || name === "write_stdin" || name === "send_input")
|
|
1584
|
+
return "shell";
|
|
1585
|
+
if (name === "apply_patch")
|
|
1586
|
+
return "file_edit";
|
|
1587
|
+
if (name === "view_image")
|
|
1588
|
+
return "file_read";
|
|
1589
|
+
if (name.includes("browser_") || name.startsWith("mcp__playwright__"))
|
|
1590
|
+
return "browser";
|
|
1591
|
+
if (name === "web_search" || name === "web_search_call")
|
|
1592
|
+
return "web";
|
|
1593
|
+
if (name === "tool_search")
|
|
1594
|
+
return "discovery";
|
|
1595
|
+
if (name === "spawn_agent" || name === "wait_agent" || name === "close_agent" || name === "resume_agent")
|
|
1596
|
+
return "subagent";
|
|
1597
|
+
if (name === "update_plan")
|
|
1598
|
+
return "planning";
|
|
1599
|
+
if (name === "create_goal" || name === "update_goal" || name === "get_goal")
|
|
1600
|
+
return "task";
|
|
1601
|
+
if (name.startsWith("mcp__"))
|
|
1602
|
+
return "mcp";
|
|
1603
|
+
return "generic";
|
|
1604
|
+
}
|
|
1605
|
+
function codexToolCompletionEvent(payload, fallbackName, timestamp) {
|
|
1606
|
+
const name = typeof payload.name === "string" ? payload.name : fallbackName;
|
|
1607
|
+
return {
|
|
1608
|
+
...(typeof payload.call_id === "string" ? { callId: payload.call_id } : {}),
|
|
1609
|
+
kind: "codex_tool_result",
|
|
1610
|
+
name,
|
|
1611
|
+
output: codexToolCompletionText(payload),
|
|
1612
|
+
status: typeof payload.status === "string" ? payload.status : undefined,
|
|
1613
|
+
toolCategory: toolCategoryForCodexName(name),
|
|
1614
|
+
...(timestamp ? { timestamp } : {})
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
function codexToolCompletionText(payload) {
|
|
1618
|
+
return [
|
|
1619
|
+
typeof payload.command === "string" ? payload.command : Array.isArray(payload.command) ? payload.command.join(" ") : undefined,
|
|
1620
|
+
typeof payload.formatted_output === "string" && payload.formatted_output.trim() ? payload.formatted_output : undefined,
|
|
1621
|
+
typeof payload.aggregated_output === "string" && payload.aggregated_output.trim() ? payload.aggregated_output : undefined,
|
|
1622
|
+
typeof payload.stdout === "string" && payload.stdout.trim() ? payload.stdout : undefined,
|
|
1623
|
+
typeof payload.stderr === "string" && payload.stderr.trim() ? payload.stderr : undefined
|
|
1624
|
+
].filter((part) => Boolean(part)).join("\n\n") || safeJson(payload);
|
|
1625
|
+
}
|
|
1626
|
+
function codexPatchChanges(value) {
|
|
1627
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1628
|
+
return [];
|
|
1629
|
+
return Object.entries(value).map(([path, change]) => {
|
|
1630
|
+
const record = change && typeof change === "object" ? change : {};
|
|
1631
|
+
return {
|
|
1632
|
+
path,
|
|
1633
|
+
text: typeof record.unified_diff === "string" ? record.unified_diff : typeof record.content === "string" ? record.content : undefined,
|
|
1634
|
+
type: typeof record.type === "string" ? record.type : "change"
|
|
1635
|
+
};
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
function codexWebSearchQuery(payload) {
|
|
1639
|
+
if (typeof payload.query === "string")
|
|
1640
|
+
return payload.query;
|
|
1641
|
+
const action = payload.action && typeof payload.action === "object" ? payload.action : undefined;
|
|
1642
|
+
if (typeof action?.query === "string")
|
|
1643
|
+
return action.query;
|
|
1644
|
+
return safeJson(payload);
|
|
1645
|
+
}
|
|
1646
|
+
function summarizeCodexSessionMeta(payload) {
|
|
1647
|
+
return summarizeKeyValues(payload, ["id", "cwd", "originator", "cli_version", "source", "model_provider"]);
|
|
1648
|
+
}
|
|
1649
|
+
function summarizeCodexTurnContext(payload) {
|
|
1650
|
+
return summarizeKeyValues(payload, ["cwd", "model", "approval_policy", "personality", "current_date", "timezone"]);
|
|
1651
|
+
}
|
|
1652
|
+
function summarizeCodexGoal(goal) {
|
|
1653
|
+
return [
|
|
1654
|
+
typeof goal.objective === "string" ? goal.objective : undefined,
|
|
1655
|
+
summarizeKeyValues(goal, ["status", "tokensUsed", "timeUsedSeconds"])
|
|
1656
|
+
].filter((part) => Boolean(part)).join("\n");
|
|
1657
|
+
}
|
|
1658
|
+
function summarizeCodexReview(payload) {
|
|
1659
|
+
if (typeof payload.user_facing_hint === "string")
|
|
1660
|
+
return payload.user_facing_hint;
|
|
1661
|
+
if (payload.review_output && typeof payload.review_output === "object")
|
|
1662
|
+
return safeJson(payload.review_output);
|
|
1663
|
+
if (payload.item && typeof payload.item === "object" && typeof payload.item.text === "string") {
|
|
1664
|
+
return payload.item.text;
|
|
1665
|
+
}
|
|
1666
|
+
return safeJson(payload);
|
|
1667
|
+
}
|
|
1668
|
+
function summarizeKeyValues(payload, keys) {
|
|
1669
|
+
return keys.map((key) => {
|
|
1670
|
+
const value = payload[key];
|
|
1671
|
+
if (value === undefined || value === null || value === "")
|
|
1672
|
+
return undefined;
|
|
1673
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
1674
|
+
return `${key}: ${value}`;
|
|
1675
|
+
return `${key}: ${safeJson(value)}`;
|
|
1676
|
+
}).filter((part) => Boolean(part)).join("\n") || safeJson(payload);
|
|
1677
|
+
}
|
|
1678
|
+
function codexEventLabel(type) {
|
|
1679
|
+
return type.split("_").map((part) => part ? part.charAt(0).toUpperCase() + part.slice(1) : part).join(" ");
|
|
1680
|
+
}
|
|
1681
|
+
function safeJson(value) {
|
|
1682
|
+
try {
|
|
1683
|
+
return JSON.stringify(value, null, 2);
|
|
1684
|
+
}
|
|
1685
|
+
catch {
|
|
1686
|
+
return String(value);
|
|
1687
|
+
}
|
|
1688
|
+
}
|