codex-devtools 0.1.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/README.md +117 -0
- package/dist-electron/main/chunks/CodexServiceContext-CRkTP14W.cjs +2027 -0
- package/dist-electron/main/index.cjs +250 -0
- package/dist-electron/main/standalone.cjs +231 -0
- package/dist-electron/preload/index.cjs +37 -0
- package/out/renderer/assets/index-CR8-JZrV.js +11905 -0
- package/out/renderer/assets/index-r_pK0Xle.css +1916 -0
- package/out/renderer/index.html +22 -0
- package/package.json +121 -0
|
@@ -0,0 +1,2027 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const path = require("node:path");
|
|
4
|
+
const node_buffer = require("node:buffer");
|
|
5
|
+
const os = require("node:os");
|
|
6
|
+
const readline = require("node:readline");
|
|
7
|
+
const node_events = require("node:events");
|
|
8
|
+
function _interopNamespaceDefault(e) {
|
|
9
|
+
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
10
|
+
if (e) {
|
|
11
|
+
for (const k in e) {
|
|
12
|
+
if (k !== "default") {
|
|
13
|
+
const d = Object.getOwnPropertyDescriptor(e, k);
|
|
14
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
get: () => e[k]
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
n.default = e;
|
|
22
|
+
return Object.freeze(n);
|
|
23
|
+
}
|
|
24
|
+
const fs__namespace = /* @__PURE__ */ _interopNamespaceDefault(fs);
|
|
25
|
+
const path__namespace = /* @__PURE__ */ _interopNamespaceDefault(path);
|
|
26
|
+
const os__namespace = /* @__PURE__ */ _interopNamespaceDefault(os);
|
|
27
|
+
const readline__namespace = /* @__PURE__ */ _interopNamespaceDefault(readline);
|
|
28
|
+
const createLogger = (scope) => {
|
|
29
|
+
const prefix = `[${scope}]`;
|
|
30
|
+
return {
|
|
31
|
+
info: (message, ...args) => console.info(prefix, message, ...args),
|
|
32
|
+
warn: (message, ...args) => console.warn(prefix, message, ...args),
|
|
33
|
+
error: (message, ...args) => console.error(prefix, message, ...args),
|
|
34
|
+
debug: (message, ...args) => console.debug(prefix, message, ...args)
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
const DEFAULT_CONFIG = {
|
|
38
|
+
general: {
|
|
39
|
+
launchAtLogin: false,
|
|
40
|
+
showDockIcon: true
|
|
41
|
+
},
|
|
42
|
+
display: {
|
|
43
|
+
showReasoning: true,
|
|
44
|
+
showTokenCounts: true,
|
|
45
|
+
showDeveloperMessages: false,
|
|
46
|
+
showAttachmentPreviews: true,
|
|
47
|
+
theme: "dark"
|
|
48
|
+
},
|
|
49
|
+
httpServer: {
|
|
50
|
+
enabled: false,
|
|
51
|
+
port: 3456
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
function deepMerge(target, source) {
|
|
55
|
+
const output = { ...target };
|
|
56
|
+
for (const [key, value] of Object.entries(source)) {
|
|
57
|
+
if (value === void 0) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value) && typeof output[key] === "object" && output[key] !== null && !Array.isArray(output[key])) {
|
|
61
|
+
output[key] = deepMerge(output[key], value);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
output[key] = value;
|
|
65
|
+
}
|
|
66
|
+
return output;
|
|
67
|
+
}
|
|
68
|
+
class ConfigManager {
|
|
69
|
+
constructor(configPath = path__namespace.join(os__namespace.homedir(), ".config", "codex-devtools", "config.json")) {
|
|
70
|
+
this.configPath = configPath;
|
|
71
|
+
this.config = this.loadConfig();
|
|
72
|
+
}
|
|
73
|
+
getConfig() {
|
|
74
|
+
return structuredClone(this.config);
|
|
75
|
+
}
|
|
76
|
+
updateConfig(partial) {
|
|
77
|
+
this.config = deepMerge(this.config, partial);
|
|
78
|
+
this.saveConfig();
|
|
79
|
+
return this.getConfig();
|
|
80
|
+
}
|
|
81
|
+
updateSection(section, partial) {
|
|
82
|
+
const currentSection = this.config[section] ?? {};
|
|
83
|
+
this.config = {
|
|
84
|
+
...this.config,
|
|
85
|
+
[section]: deepMerge(currentSection, partial)
|
|
86
|
+
};
|
|
87
|
+
this.saveConfig();
|
|
88
|
+
return this.getConfig();
|
|
89
|
+
}
|
|
90
|
+
resetConfig() {
|
|
91
|
+
this.config = structuredClone(DEFAULT_CONFIG);
|
|
92
|
+
this.saveConfig();
|
|
93
|
+
return this.getConfig();
|
|
94
|
+
}
|
|
95
|
+
loadConfig() {
|
|
96
|
+
if (!fs__namespace.existsSync(this.configPath)) {
|
|
97
|
+
const initial = structuredClone(DEFAULT_CONFIG);
|
|
98
|
+
this.writeConfig(initial);
|
|
99
|
+
return initial;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const raw = fs__namespace.readFileSync(this.configPath, "utf8");
|
|
103
|
+
const parsed = JSON.parse(raw);
|
|
104
|
+
return deepMerge(structuredClone(DEFAULT_CONFIG), parsed);
|
|
105
|
+
} catch {
|
|
106
|
+
const fallback = structuredClone(DEFAULT_CONFIG);
|
|
107
|
+
this.writeConfig(fallback);
|
|
108
|
+
return fallback;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
saveConfig() {
|
|
112
|
+
this.writeConfig(this.config);
|
|
113
|
+
}
|
|
114
|
+
writeConfig(config) {
|
|
115
|
+
const dir = path__namespace.dirname(this.configPath);
|
|
116
|
+
fs__namespace.mkdirSync(dir, { recursive: true });
|
|
117
|
+
fs__namespace.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf8");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function isRecord(value) {
|
|
121
|
+
return typeof value === "object" && value !== null;
|
|
122
|
+
}
|
|
123
|
+
function isString(value) {
|
|
124
|
+
return typeof value === "string";
|
|
125
|
+
}
|
|
126
|
+
function hasString(obj, key) {
|
|
127
|
+
return isString(obj[key]);
|
|
128
|
+
}
|
|
129
|
+
function hasOptionalString(obj, key) {
|
|
130
|
+
return !(key in obj) || isString(obj[key]);
|
|
131
|
+
}
|
|
132
|
+
function hasOptionalRecord(obj, key) {
|
|
133
|
+
return !(key in obj) || isRecord(obj[key]);
|
|
134
|
+
}
|
|
135
|
+
function getContentBlockText(block) {
|
|
136
|
+
if (!("text" in block)) {
|
|
137
|
+
return "";
|
|
138
|
+
}
|
|
139
|
+
return isString(block.text) ? block.text : "";
|
|
140
|
+
}
|
|
141
|
+
function isReasoningSummaryText(value) {
|
|
142
|
+
return isRecord(value) && hasString(value, "text");
|
|
143
|
+
}
|
|
144
|
+
function reasoningSummaryItemToText(item) {
|
|
145
|
+
return isString(item) ? item : item.text;
|
|
146
|
+
}
|
|
147
|
+
function reasoningSummaryToText(summary) {
|
|
148
|
+
return summary.map(reasoningSummaryItemToText);
|
|
149
|
+
}
|
|
150
|
+
function isContentBlockInputText(value) {
|
|
151
|
+
if (!isRecord(value)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
return value.type === "input_text" && hasString(value, "text");
|
|
155
|
+
}
|
|
156
|
+
function isContentBlockOutputText(value) {
|
|
157
|
+
if (!isRecord(value)) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
return value.type === "output_text" && hasString(value, "text");
|
|
161
|
+
}
|
|
162
|
+
function isContentBlockInputImage(value) {
|
|
163
|
+
if (!isRecord(value)) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return value.type === "input_image" && hasString(value, "image_url");
|
|
167
|
+
}
|
|
168
|
+
function isContentBlockUnknown(value) {
|
|
169
|
+
if (!isRecord(value) || !hasString(value, "type")) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
if (value.type === "input_text" || value.type === "output_text" || value.type === "input_image") {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
return !("text" in value) || isString(value.text);
|
|
176
|
+
}
|
|
177
|
+
function isContentBlock(value) {
|
|
178
|
+
return isContentBlockInputText(value) || isContentBlockOutputText(value) || isContentBlockInputImage(value) || isContentBlockUnknown(value);
|
|
179
|
+
}
|
|
180
|
+
function isMessagePayload(value) {
|
|
181
|
+
if (!isRecord(value)) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
if (value.type !== "message" || value.role !== "developer" && value.role !== "user" && value.role !== "assistant") {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
if (!Array.isArray(value.content)) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
return value.content.every(isContentBlock);
|
|
191
|
+
}
|
|
192
|
+
function isFunctionCallPayload(value) {
|
|
193
|
+
if (!isRecord(value)) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
return value.type === "function_call" && hasString(value, "name") && hasString(value, "arguments") && hasString(value, "call_id");
|
|
197
|
+
}
|
|
198
|
+
function isFunctionCallOutputPayload(value) {
|
|
199
|
+
if (!isRecord(value)) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
return value.type === "function_call_output" && hasString(value, "call_id") && hasString(value, "output");
|
|
203
|
+
}
|
|
204
|
+
function isReasoningPayload(value) {
|
|
205
|
+
if (!isRecord(value)) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
if (value.type !== "reasoning" || !Array.isArray(value.summary)) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
if (!value.summary.every((item) => isString(item) || isReasoningSummaryText(item))) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
return !("encrypted_content" in value) || isString(value.encrypted_content) || value.encrypted_content === null;
|
|
215
|
+
}
|
|
216
|
+
function isResponseItemPayload(value) {
|
|
217
|
+
return isMessagePayload(value) || isFunctionCallPayload(value) || isFunctionCallOutputPayload(value) || isReasoningPayload(value);
|
|
218
|
+
}
|
|
219
|
+
function isTokenUsage(value) {
|
|
220
|
+
if (!isRecord(value)) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
return typeof value.input_tokens === "number" && typeof value.cached_input_tokens === "number" && typeof value.output_tokens === "number" && typeof value.reasoning_output_tokens === "number" && typeof value.total_tokens === "number";
|
|
224
|
+
}
|
|
225
|
+
function isTokenCountPayload(value) {
|
|
226
|
+
if (!isRecord(value) || value.type !== "token_count") {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
if (value.info === null) {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
if (!isRecord(value.info)) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
return isTokenUsage(value.info.total_token_usage) && isTokenUsage(value.info.last_token_usage) && typeof value.info.model_context_window === "number";
|
|
236
|
+
}
|
|
237
|
+
function isAgentReasoningPayload(value) {
|
|
238
|
+
if (!isRecord(value)) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
return value.type === "agent_reasoning" && hasString(value, "text");
|
|
242
|
+
}
|
|
243
|
+
function isAgentMessagePayload(value) {
|
|
244
|
+
if (!isRecord(value)) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
return value.type === "agent_message" && hasString(value, "message");
|
|
248
|
+
}
|
|
249
|
+
function isUserMessagePayload(value) {
|
|
250
|
+
if (!isRecord(value)) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
return value.type === "user_message" && hasString(value, "message");
|
|
254
|
+
}
|
|
255
|
+
function isContextCompactedPayload(value) {
|
|
256
|
+
if (!isRecord(value)) {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
return value.type === "context_compacted";
|
|
260
|
+
}
|
|
261
|
+
function isEventMsgPayload(value) {
|
|
262
|
+
return isTokenCountPayload(value) || isAgentReasoningPayload(value) || isAgentMessagePayload(value) || isUserMessagePayload(value) || isContextCompactedPayload(value);
|
|
263
|
+
}
|
|
264
|
+
function isSessionMetaEntry(value) {
|
|
265
|
+
if (!isRecord(value) || value.type !== "session_meta" || !hasString(value, "timestamp")) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
if (!isRecord(value.payload)) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
if (!hasOptionalString(value.payload, "id") || !hasOptionalString(value.payload, "cwd") || !hasOptionalString(value.payload, "originator") || !hasOptionalString(value.payload, "cli_version") || !hasOptionalString(value.payload, "model_provider") || !hasOptionalString(value.payload, "model")) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
if ("base_instructions" in value.payload) {
|
|
275
|
+
const baseInstructions = value.payload.base_instructions;
|
|
276
|
+
if (!isString(baseInstructions) && !isRecord(baseInstructions)) {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (!hasOptionalRecord(value.payload, "git")) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
if (isRecord(value.payload.git)) {
|
|
284
|
+
if (!hasOptionalString(value.payload.git, "commit_hash") || !hasOptionalString(value.payload.git, "branch") || !hasOptionalString(value.payload.git, "repository_url")) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return hasString(value.payload, "id") || hasString(value.payload, "cwd");
|
|
289
|
+
}
|
|
290
|
+
function isResponseItemEntry(value) {
|
|
291
|
+
if (!isRecord(value) || value.type !== "response_item" || !hasString(value, "timestamp")) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
return isResponseItemPayload(value.payload);
|
|
295
|
+
}
|
|
296
|
+
function isTurnContextEntry(value) {
|
|
297
|
+
if (!isRecord(value) || value.type !== "turn_context" || !hasString(value, "timestamp")) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
if (!isRecord(value.payload)) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
if (!hasOptionalString(value.payload, "turn_id") || !hasOptionalString(value.payload, "cwd") || !hasOptionalString(value.payload, "approval_policy") || !hasOptionalString(value.payload, "model") || !hasOptionalString(value.payload, "personality") || !hasOptionalString(value.payload, "effort") || !hasOptionalString(value.payload, "summary") || !hasOptionalString(value.payload, "user_instructions")) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
const sandboxPolicy = value.payload.sandbox_policy;
|
|
307
|
+
if ("sandbox_policy" in value.payload && !(isString(sandboxPolicy) || isRecord(sandboxPolicy))) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
const collaborationMode = value.payload.collaboration_mode;
|
|
311
|
+
if ("collaboration_mode" in value.payload && !(isString(collaborationMode) || isRecord(collaborationMode))) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
const truncationPolicy = value.payload.truncation_policy;
|
|
315
|
+
if ("truncation_policy" in value.payload && !(isString(truncationPolicy) || isRecord(truncationPolicy))) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
return hasString(value.payload, "cwd") || hasString(value.payload, "model");
|
|
319
|
+
}
|
|
320
|
+
function isEventMsgEntry(value) {
|
|
321
|
+
if (!isRecord(value) || value.type !== "event_msg" || !hasString(value, "timestamp")) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
return isEventMsgPayload(value.payload);
|
|
325
|
+
}
|
|
326
|
+
function isCompactedEntry(value) {
|
|
327
|
+
if (!isRecord(value) || value.type !== "compacted" || !hasString(value, "timestamp")) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
return isRecord(value.payload);
|
|
331
|
+
}
|
|
332
|
+
function isCompactionEntry(value) {
|
|
333
|
+
if (!isRecord(value) || value.type !== "compaction" || !hasString(value, "timestamp")) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
if ("payload" in value && !isRecord(value.payload)) {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
if ("encrypted_content" in value && !(isString(value.encrypted_content) || value.encrypted_content === null)) {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
function isCodexLogEntry(value) {
|
|
345
|
+
return isSessionMetaEntry(value) || isResponseItemEntry(value) || isTurnContextEntry(value) || isEventMsgEntry(value) || isCompactedEntry(value) || isCompactionEntry(value);
|
|
346
|
+
}
|
|
347
|
+
const EMPTY_CODEX_SESSION_METRICS = {
|
|
348
|
+
totalTokens: 0,
|
|
349
|
+
inputTokens: 0,
|
|
350
|
+
outputTokens: 0,
|
|
351
|
+
cachedTokens: 0,
|
|
352
|
+
reasoningTokens: 0,
|
|
353
|
+
turnCount: 0,
|
|
354
|
+
toolCallCount: 0,
|
|
355
|
+
duration: 0
|
|
356
|
+
};
|
|
357
|
+
const AGENTS_HEADING_PATTERN = /^#?\s*AGENTS\.md instructions\b/i;
|
|
358
|
+
const AGENTS_INSTRUCTIONS_BLOCK_PATTERN = /<INSTRUCTIONS>[\s\S]*<\/INSTRUCTIONS>/i;
|
|
359
|
+
const ENVIRONMENT_CONTEXT_WRAPPER_PATTERN = /^<environment_context>[\s\S]*<\/environment_context>$/i;
|
|
360
|
+
const ENVIRONMENT_CONTEXT_CWD_PATTERN = /<cwd>[\s\S]*<\/cwd>/i;
|
|
361
|
+
const ENVIRONMENT_CONTEXT_SHELL_PATTERN = /<shell>[\s\S]*<\/shell>/i;
|
|
362
|
+
const PERMISSIONS_INSTRUCTIONS_WRAPPER_PATTERN = /^<permissions\s+instructions>[\s\S]*<\/permissions\s+instructions>$/i;
|
|
363
|
+
const COLLABORATION_MODE_WRAPPER_PATTERN = /^<collaboration_mode>[\s\S]*<\/collaboration_mode>$/i;
|
|
364
|
+
const TURN_ABORTED_WRAPPER_PATTERN = /^<turn_aborted>[\s\S]*<\/turn_aborted>$/i;
|
|
365
|
+
function classifyCodexBootstrapMessage(content) {
|
|
366
|
+
const value = content.trim();
|
|
367
|
+
if (!value) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
if (AGENTS_HEADING_PATTERN.test(value) && AGENTS_INSTRUCTIONS_BLOCK_PATTERN.test(value)) {
|
|
371
|
+
return "agents_instructions";
|
|
372
|
+
}
|
|
373
|
+
if (ENVIRONMENT_CONTEXT_WRAPPER_PATTERN.test(value) && ENVIRONMENT_CONTEXT_CWD_PATTERN.test(value) && ENVIRONMENT_CONTEXT_SHELL_PATTERN.test(value)) {
|
|
374
|
+
return "environment_context";
|
|
375
|
+
}
|
|
376
|
+
if (PERMISSIONS_INSTRUCTIONS_WRAPPER_PATTERN.test(value)) {
|
|
377
|
+
return "permissions_instructions";
|
|
378
|
+
}
|
|
379
|
+
if (COLLABORATION_MODE_WRAPPER_PATTERN.test(value)) {
|
|
380
|
+
return "collaboration_mode";
|
|
381
|
+
}
|
|
382
|
+
if (TURN_ABORTED_WRAPPER_PATTERN.test(value)) {
|
|
383
|
+
return "turn_aborted";
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
class CodexMessageClassifier {
|
|
388
|
+
classifyEntry(entry) {
|
|
389
|
+
if (this.isUserMessage(entry)) {
|
|
390
|
+
return { entry, kind: "user" };
|
|
391
|
+
}
|
|
392
|
+
if (this.isAssistantMessage(entry)) {
|
|
393
|
+
return { entry, kind: "assistant" };
|
|
394
|
+
}
|
|
395
|
+
if (this.isFunctionCall(entry)) {
|
|
396
|
+
return { entry, kind: "function_call" };
|
|
397
|
+
}
|
|
398
|
+
if (this.isFunctionOutput(entry)) {
|
|
399
|
+
return { entry, kind: "function_output" };
|
|
400
|
+
}
|
|
401
|
+
if (this.isReasoning(entry)) {
|
|
402
|
+
return { entry, kind: "reasoning" };
|
|
403
|
+
}
|
|
404
|
+
if (isResponseItemEntry(entry) && isMessagePayload(entry.payload) && entry.payload.role === "developer") {
|
|
405
|
+
return { entry, kind: "developer" };
|
|
406
|
+
}
|
|
407
|
+
if (isEventMsgEntry(entry)) {
|
|
408
|
+
return { entry, kind: "event" };
|
|
409
|
+
}
|
|
410
|
+
return { entry, kind: "other" };
|
|
411
|
+
}
|
|
412
|
+
classifyEntries(entries) {
|
|
413
|
+
return entries.map((entry) => this.classifyEntry(entry));
|
|
414
|
+
}
|
|
415
|
+
isUserMessage(entry) {
|
|
416
|
+
if (isResponseItemEntry(entry) && isMessagePayload(entry.payload)) {
|
|
417
|
+
return entry.payload.role === "user";
|
|
418
|
+
}
|
|
419
|
+
return isEventMsgEntry(entry) && isUserMessagePayload(entry.payload);
|
|
420
|
+
}
|
|
421
|
+
isAssistantMessage(entry) {
|
|
422
|
+
if (isResponseItemEntry(entry) && isMessagePayload(entry.payload)) {
|
|
423
|
+
return entry.payload.role === "assistant";
|
|
424
|
+
}
|
|
425
|
+
if (!isEventMsgEntry(entry)) {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
return isAgentMessagePayload(entry.payload) || isAgentReasoningPayload(entry.payload);
|
|
429
|
+
}
|
|
430
|
+
isFunctionCall(entry) {
|
|
431
|
+
return isResponseItemEntry(entry) && isFunctionCallPayload(entry.payload);
|
|
432
|
+
}
|
|
433
|
+
isFunctionOutput(entry) {
|
|
434
|
+
return isResponseItemEntry(entry) && isFunctionCallOutputPayload(entry.payload);
|
|
435
|
+
}
|
|
436
|
+
isReasoning(entry) {
|
|
437
|
+
if (isResponseItemEntry(entry) && isReasoningPayload(entry.payload)) {
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
return isEventMsgEntry(entry) && isAgentReasoningPayload(entry.payload);
|
|
441
|
+
}
|
|
442
|
+
isTokenCountEvent(entry) {
|
|
443
|
+
return isEventMsgEntry(entry) && entry.payload.type === "token_count";
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const IMAGE_TAG_PATTERN = /<\/?image\b[^>]*>/gi;
|
|
447
|
+
const IMAGE_PLACEHOLDER_PATTERN = /\[(?:Image|Attachment) #\d+\]/gi;
|
|
448
|
+
const MAX_ATTACHMENT_PREVIEW_BYTES = 2 * 1024 * 1024;
|
|
449
|
+
const MAX_TEXT_ATTACHMENT_PREVIEW_CHARS = 2e4;
|
|
450
|
+
const MARKDOWN_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
451
|
+
"text/markdown",
|
|
452
|
+
"text/x-markdown",
|
|
453
|
+
"application/markdown"
|
|
454
|
+
]);
|
|
455
|
+
const CODE_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
456
|
+
"application/json",
|
|
457
|
+
"application/xml",
|
|
458
|
+
"application/javascript",
|
|
459
|
+
"application/x-javascript",
|
|
460
|
+
"application/typescript",
|
|
461
|
+
"application/x-typescript",
|
|
462
|
+
"application/x-sh",
|
|
463
|
+
"application/x-shellscript",
|
|
464
|
+
"application/x-python",
|
|
465
|
+
"application/x-rust",
|
|
466
|
+
"application/x-go",
|
|
467
|
+
"application/x-yaml",
|
|
468
|
+
"application/yaml",
|
|
469
|
+
"application/x-toml"
|
|
470
|
+
]);
|
|
471
|
+
const CODE_TEXT_SUBTYPES = /* @__PURE__ */ new Set([
|
|
472
|
+
"x-python",
|
|
473
|
+
"x-sh",
|
|
474
|
+
"x-shellscript",
|
|
475
|
+
"x-go",
|
|
476
|
+
"x-rust",
|
|
477
|
+
"x-java",
|
|
478
|
+
"x-c",
|
|
479
|
+
"x-c++",
|
|
480
|
+
"x-csharp",
|
|
481
|
+
"javascript",
|
|
482
|
+
"typescript",
|
|
483
|
+
"html",
|
|
484
|
+
"css",
|
|
485
|
+
"xml",
|
|
486
|
+
"yaml",
|
|
487
|
+
"x-yaml",
|
|
488
|
+
"toml",
|
|
489
|
+
"csv"
|
|
490
|
+
]);
|
|
491
|
+
function parseTimestampMs$1(timestamp) {
|
|
492
|
+
const ms = new Date(timestamp).getTime();
|
|
493
|
+
return Number.isNaN(ms) ? 0 : ms;
|
|
494
|
+
}
|
|
495
|
+
function normalizeComparableText(value) {
|
|
496
|
+
return value.replace(/\s+/g, " ").trim();
|
|
497
|
+
}
|
|
498
|
+
function sanitizeUserText(value) {
|
|
499
|
+
return value.replace(IMAGE_TAG_PATTERN, "").replace(/[ \t]+\n/g, "\n").replace(/\n[ \t]+/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
500
|
+
}
|
|
501
|
+
function countImagePlaceholders(value) {
|
|
502
|
+
const matches = sanitizeUserText(value).match(IMAGE_PLACEHOLDER_PATTERN);
|
|
503
|
+
return matches?.length ?? 0;
|
|
504
|
+
}
|
|
505
|
+
function normalizeUserTextWithoutImagePlaceholders(value) {
|
|
506
|
+
return normalizeComparableText(
|
|
507
|
+
sanitizeUserText(value).replace(IMAGE_PLACEHOLDER_PATTERN, " ")
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
function normalizeBase64Payload(value) {
|
|
511
|
+
const compact = value.replace(/\s+/g, "");
|
|
512
|
+
const remainder = compact.length % 4;
|
|
513
|
+
if (remainder === 0) {
|
|
514
|
+
return compact;
|
|
515
|
+
}
|
|
516
|
+
return `${compact}${"=".repeat(4 - remainder)}`;
|
|
517
|
+
}
|
|
518
|
+
function estimateBase64DecodedBytes(value) {
|
|
519
|
+
const normalized = normalizeBase64Payload(value);
|
|
520
|
+
const padding = normalized.endsWith("==") ? 2 : normalized.endsWith("=") ? 1 : 0;
|
|
521
|
+
return Math.max(0, Math.floor(normalized.length * 3 / 4) - padding);
|
|
522
|
+
}
|
|
523
|
+
function parseBase64DataUrl(value) {
|
|
524
|
+
const trimmed = value.trim();
|
|
525
|
+
if (!trimmed.toLowerCase().startsWith("data:")) {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
const commaIndex = trimmed.indexOf(",");
|
|
529
|
+
if (commaIndex < 0) {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
const metadata = trimmed.slice(5, commaIndex);
|
|
533
|
+
const payload = trimmed.slice(commaIndex + 1);
|
|
534
|
+
if (!/;base64$/i.test(metadata) && !/;base64;/i.test(metadata)) {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
const base64Payload = normalizeBase64Payload(payload);
|
|
538
|
+
if (!base64Payload || /[^a-zA-Z0-9+/=]/.test(base64Payload)) {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
const mimeType = metadata.split(";")[0]?.trim().toLowerCase() || "application/octet-stream";
|
|
542
|
+
return {
|
|
543
|
+
mimeType,
|
|
544
|
+
base64Payload,
|
|
545
|
+
dataUrl: `data:${metadata},${base64Payload}`
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
function inferAttachmentKind(mimeType) {
|
|
549
|
+
if (mimeType.startsWith("image/")) {
|
|
550
|
+
return "image";
|
|
551
|
+
}
|
|
552
|
+
if (MARKDOWN_MIME_TYPES.has(mimeType)) {
|
|
553
|
+
return "markdown";
|
|
554
|
+
}
|
|
555
|
+
if (CODE_MIME_TYPES.has(mimeType)) {
|
|
556
|
+
return "code";
|
|
557
|
+
}
|
|
558
|
+
if (mimeType.startsWith("text/")) {
|
|
559
|
+
const subtype = mimeType.slice("text/".length);
|
|
560
|
+
if (subtype === "markdown" || subtype === "x-markdown") {
|
|
561
|
+
return "markdown";
|
|
562
|
+
}
|
|
563
|
+
return CODE_TEXT_SUBTYPES.has(subtype) ? "code" : "text";
|
|
564
|
+
}
|
|
565
|
+
if (mimeType === "application/octet-stream") {
|
|
566
|
+
return "binary";
|
|
567
|
+
}
|
|
568
|
+
if (mimeType.startsWith("application/")) {
|
|
569
|
+
return "binary";
|
|
570
|
+
}
|
|
571
|
+
return "unknown";
|
|
572
|
+
}
|
|
573
|
+
function truncateTextPreview(value) {
|
|
574
|
+
if (value.length <= MAX_TEXT_ATTACHMENT_PREVIEW_CHARS) {
|
|
575
|
+
return value;
|
|
576
|
+
}
|
|
577
|
+
return `${value.slice(0, MAX_TEXT_ATTACHMENT_PREVIEW_CHARS)}
|
|
578
|
+
|
|
579
|
+
… preview truncated`;
|
|
580
|
+
}
|
|
581
|
+
function decodeBase64Text(value) {
|
|
582
|
+
try {
|
|
583
|
+
const decoded = node_buffer.Buffer.from(normalizeBase64Payload(value), "base64").toString("utf8");
|
|
584
|
+
return truncateTextPreview(decoded);
|
|
585
|
+
} catch {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function attachmentFingerprint(attachment) {
|
|
590
|
+
const previewSample = attachment.dataUrl ? attachment.dataUrl.slice(0, 96) : attachment.textContent ? attachment.textContent.slice(0, 96) : "";
|
|
591
|
+
return [
|
|
592
|
+
attachment.mimeType,
|
|
593
|
+
attachment.kind,
|
|
594
|
+
attachment.sizeBytes ?? -1,
|
|
595
|
+
attachment.previewable ? "1" : "0",
|
|
596
|
+
attachment.previewReason ?? "",
|
|
597
|
+
previewSample
|
|
598
|
+
].join("|");
|
|
599
|
+
}
|
|
600
|
+
function mergeAttachments(primary, incoming) {
|
|
601
|
+
if (primary.length === 0) {
|
|
602
|
+
return [...incoming];
|
|
603
|
+
}
|
|
604
|
+
if (incoming.length === 0) {
|
|
605
|
+
return [...primary];
|
|
606
|
+
}
|
|
607
|
+
const merged = [...primary];
|
|
608
|
+
const seen = new Set(primary.map((attachment) => attachmentFingerprint(attachment)));
|
|
609
|
+
for (const attachment of incoming) {
|
|
610
|
+
const fingerprint = attachmentFingerprint(attachment);
|
|
611
|
+
if (seen.has(fingerprint)) {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
seen.add(fingerprint);
|
|
615
|
+
merged.push(attachment);
|
|
616
|
+
}
|
|
617
|
+
return merged;
|
|
618
|
+
}
|
|
619
|
+
function withPreviewDisabledReason(attachment, previewReason) {
|
|
620
|
+
return {
|
|
621
|
+
...attachment,
|
|
622
|
+
previewable: false,
|
|
623
|
+
previewReason,
|
|
624
|
+
dataUrl: void 0,
|
|
625
|
+
textContent: void 0
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
function buildAttachmentFromDataUrl(dataUrl, attachmentId, source) {
|
|
629
|
+
const parsed = parseBase64DataUrl(dataUrl);
|
|
630
|
+
if (!parsed) {
|
|
631
|
+
return {
|
|
632
|
+
id: attachmentId,
|
|
633
|
+
source,
|
|
634
|
+
mimeType: "application/octet-stream",
|
|
635
|
+
kind: "unknown",
|
|
636
|
+
encoding: "base64",
|
|
637
|
+
sizeBytes: null,
|
|
638
|
+
previewable: false,
|
|
639
|
+
previewReason: "decode_error",
|
|
640
|
+
fileName: null
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
const kind = inferAttachmentKind(parsed.mimeType);
|
|
644
|
+
const sizeBytes = estimateBase64DecodedBytes(parsed.base64Payload);
|
|
645
|
+
const baseAttachment = {
|
|
646
|
+
id: attachmentId,
|
|
647
|
+
source,
|
|
648
|
+
mimeType: parsed.mimeType,
|
|
649
|
+
kind,
|
|
650
|
+
encoding: "base64",
|
|
651
|
+
sizeBytes,
|
|
652
|
+
previewable: false,
|
|
653
|
+
fileName: null
|
|
654
|
+
};
|
|
655
|
+
if (sizeBytes > MAX_ATTACHMENT_PREVIEW_BYTES) {
|
|
656
|
+
return withPreviewDisabledReason(baseAttachment, "too_large");
|
|
657
|
+
}
|
|
658
|
+
if (kind === "image") {
|
|
659
|
+
return {
|
|
660
|
+
...baseAttachment,
|
|
661
|
+
previewable: true,
|
|
662
|
+
dataUrl: parsed.dataUrl
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
if (kind === "text" || kind === "markdown" || kind === "code") {
|
|
666
|
+
const textContent = decodeBase64Text(parsed.base64Payload);
|
|
667
|
+
if (textContent === null) {
|
|
668
|
+
return withPreviewDisabledReason(baseAttachment, "decode_error");
|
|
669
|
+
}
|
|
670
|
+
return {
|
|
671
|
+
...baseAttachment,
|
|
672
|
+
previewable: true,
|
|
673
|
+
textContent
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
if (kind === "binary") {
|
|
677
|
+
return withPreviewDisabledReason(baseAttachment, "binary");
|
|
678
|
+
}
|
|
679
|
+
return withPreviewDisabledReason(baseAttachment, "unsupported_mime");
|
|
680
|
+
}
|
|
681
|
+
function extractAttachmentsFromResponseUser(entry, timestamp) {
|
|
682
|
+
if (!isResponseItemEntry(entry) || !isMessagePayload(entry.payload) || entry.payload.role !== "user") {
|
|
683
|
+
return [];
|
|
684
|
+
}
|
|
685
|
+
const attachments = [];
|
|
686
|
+
let attachmentIndex = 0;
|
|
687
|
+
for (const block of entry.payload.content) {
|
|
688
|
+
if (block.type !== "input_image") {
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
if (!("image_url" in block) || typeof block.image_url !== "string") {
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
attachmentIndex += 1;
|
|
695
|
+
attachments.push(
|
|
696
|
+
buildAttachmentFromDataUrl(
|
|
697
|
+
block.image_url,
|
|
698
|
+
`${timestamp}-attachment-${attachmentIndex}`,
|
|
699
|
+
"response_item"
|
|
700
|
+
)
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
return attachments;
|
|
704
|
+
}
|
|
705
|
+
function attachmentPlaceholder(attachment, index) {
|
|
706
|
+
if (attachment.kind === "image") {
|
|
707
|
+
return `[Image #${index + 1}]`;
|
|
708
|
+
}
|
|
709
|
+
return `[Attachment #${index + 1}]`;
|
|
710
|
+
}
|
|
711
|
+
function isEquivalentUserContent(left, right) {
|
|
712
|
+
const normalizedLeft = normalizeComparableText(sanitizeUserText(left));
|
|
713
|
+
const normalizedRight = normalizeComparableText(sanitizeUserText(right));
|
|
714
|
+
if (normalizedLeft === normalizedRight) {
|
|
715
|
+
return true;
|
|
716
|
+
}
|
|
717
|
+
const textOnlyLeft = normalizeUserTextWithoutImagePlaceholders(left);
|
|
718
|
+
const textOnlyRight = normalizeUserTextWithoutImagePlaceholders(right);
|
|
719
|
+
if (textOnlyLeft && textOnlyLeft === textOnlyRight) {
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
if (!textOnlyLeft && !textOnlyRight) {
|
|
723
|
+
const placeholderCountLeft = countImagePlaceholders(left);
|
|
724
|
+
const placeholderCountRight = countImagePlaceholders(right);
|
|
725
|
+
return placeholderCountLeft > 0 && placeholderCountLeft === placeholderCountRight;
|
|
726
|
+
}
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
function pickPreferredEquivalentUserContent(pending, incoming) {
|
|
730
|
+
const pendingPlaceholderCount = countImagePlaceholders(pending.content);
|
|
731
|
+
const incomingPlaceholderCount = countImagePlaceholders(incoming.content);
|
|
732
|
+
if (incomingPlaceholderCount !== pendingPlaceholderCount) {
|
|
733
|
+
return incomingPlaceholderCount > pendingPlaceholderCount ? incoming : pending;
|
|
734
|
+
}
|
|
735
|
+
if (pending.source === "event" && incoming.source === "response") {
|
|
736
|
+
return incoming;
|
|
737
|
+
}
|
|
738
|
+
return pending;
|
|
739
|
+
}
|
|
740
|
+
function normalizeModel$2(model) {
|
|
741
|
+
return model?.trim() ?? "";
|
|
742
|
+
}
|
|
743
|
+
function normalizeReasoningEffort$2(effort) {
|
|
744
|
+
const value = effort?.trim();
|
|
745
|
+
return value ? value : "unknown";
|
|
746
|
+
}
|
|
747
|
+
function normalizeCollaborationMode(value) {
|
|
748
|
+
if (!value) {
|
|
749
|
+
return "";
|
|
750
|
+
}
|
|
751
|
+
if (typeof value === "string") {
|
|
752
|
+
return value.trim();
|
|
753
|
+
}
|
|
754
|
+
const mode = value.mode;
|
|
755
|
+
if (typeof mode === "string") {
|
|
756
|
+
return mode.trim();
|
|
757
|
+
}
|
|
758
|
+
return "";
|
|
759
|
+
}
|
|
760
|
+
function pushUniqueAdjacent(blocks, value) {
|
|
761
|
+
const text = value.trim();
|
|
762
|
+
if (!text) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
const last = blocks[blocks.length - 1];
|
|
766
|
+
if (last && normalizeComparableText(last) === normalizeComparableText(text)) {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
blocks.push(text);
|
|
770
|
+
}
|
|
771
|
+
function addMessageSection(ai, source, blocks) {
|
|
772
|
+
const textBlocks = [];
|
|
773
|
+
for (const block of blocks) {
|
|
774
|
+
pushUniqueAdjacent(textBlocks, block);
|
|
775
|
+
}
|
|
776
|
+
if (textBlocks.length === 0) {
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
ai.sections.push({
|
|
780
|
+
kind: "message",
|
|
781
|
+
source,
|
|
782
|
+
textBlocks
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
function addReasoningSection(ai, source, summaries) {
|
|
786
|
+
const nextSummaries = [];
|
|
787
|
+
for (const summary of summaries) {
|
|
788
|
+
pushUniqueAdjacent(nextSummaries, summary);
|
|
789
|
+
}
|
|
790
|
+
if (nextSummaries.length === 0) {
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
const last = ai.sections[ai.sections.length - 1];
|
|
794
|
+
if (last?.kind === "reasoning" && last.source === source) {
|
|
795
|
+
for (const summary of nextSummaries) {
|
|
796
|
+
pushUniqueAdjacent(last.summaries, summary);
|
|
797
|
+
}
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
ai.sections.push({
|
|
801
|
+
kind: "reasoning",
|
|
802
|
+
source,
|
|
803
|
+
summaries: nextSummaries
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
function addToolSectionIndex(ai, toolIndex) {
|
|
807
|
+
for (const section of ai.sections) {
|
|
808
|
+
if (section.kind === "tools" && section.toolIndexes.includes(toolIndex)) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
const last = ai.sections[ai.sections.length - 1];
|
|
813
|
+
if (last?.kind === "tools") {
|
|
814
|
+
if (!last.toolIndexes.includes(toolIndex)) {
|
|
815
|
+
last.toolIndexes.push(toolIndex);
|
|
816
|
+
}
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
ai.sections.push({
|
|
820
|
+
kind: "tools",
|
|
821
|
+
toolIndexes: [toolIndex]
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
function shouldInsertCompactionChunk(chunks, timestamp) {
|
|
825
|
+
const last = chunks[chunks.length - 1];
|
|
826
|
+
if (!last || last.type !== "compaction") {
|
|
827
|
+
return true;
|
|
828
|
+
}
|
|
829
|
+
const deltaMs = Math.abs(parseTimestampMs$1(timestamp) - parseTimestampMs$1(last.timestamp));
|
|
830
|
+
return deltaMs > 1e3;
|
|
831
|
+
}
|
|
832
|
+
function buildOrderedAISections(ai) {
|
|
833
|
+
const hasResponseMessages = ai.sections.some(
|
|
834
|
+
(section) => section.kind === "message" && section.source === "response"
|
|
835
|
+
);
|
|
836
|
+
const hasResponseReasoning = ai.sections.some(
|
|
837
|
+
(section) => section.kind === "reasoning" && section.source === "response"
|
|
838
|
+
);
|
|
839
|
+
const selected = ai.sections.filter((section) => {
|
|
840
|
+
if (section.kind === "message") {
|
|
841
|
+
return section.source === (hasResponseMessages ? "response" : "event");
|
|
842
|
+
}
|
|
843
|
+
if (section.kind === "reasoning") {
|
|
844
|
+
return section.source === (hasResponseReasoning ? "response" : "event");
|
|
845
|
+
}
|
|
846
|
+
return true;
|
|
847
|
+
});
|
|
848
|
+
const merged = [];
|
|
849
|
+
for (const section of selected) {
|
|
850
|
+
if (section.kind === "message") {
|
|
851
|
+
const textBlocks = [];
|
|
852
|
+
for (const block of section.textBlocks) {
|
|
853
|
+
pushUniqueAdjacent(textBlocks, block);
|
|
854
|
+
}
|
|
855
|
+
if (textBlocks.length > 0) {
|
|
856
|
+
merged.push({
|
|
857
|
+
type: "message",
|
|
858
|
+
textBlocks
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (section.kind === "reasoning") {
|
|
864
|
+
const last2 = merged[merged.length - 1];
|
|
865
|
+
if (last2?.type === "reasoning") {
|
|
866
|
+
for (const summary of section.summaries) {
|
|
867
|
+
pushUniqueAdjacent(last2.summaries, summary);
|
|
868
|
+
}
|
|
869
|
+
} else {
|
|
870
|
+
const summaries = [];
|
|
871
|
+
for (const summary of section.summaries) {
|
|
872
|
+
pushUniqueAdjacent(summaries, summary);
|
|
873
|
+
}
|
|
874
|
+
if (summaries.length > 0) {
|
|
875
|
+
merged.push({
|
|
876
|
+
type: "reasoning",
|
|
877
|
+
summaries
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
const executions = section.toolIndexes.map((index) => ai.toolExecutions[index]).filter((execution) => Boolean(execution));
|
|
884
|
+
if (executions.length === 0) {
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
const last = merged[merged.length - 1];
|
|
888
|
+
if (last?.type === "tool_executions") {
|
|
889
|
+
const seenCallIds = new Set(last.executions.map((execution) => execution.functionCall.callId));
|
|
890
|
+
for (const execution of executions) {
|
|
891
|
+
if (seenCallIds.has(execution.functionCall.callId)) {
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
seenCallIds.add(execution.functionCall.callId);
|
|
895
|
+
last.executions.push(execution);
|
|
896
|
+
}
|
|
897
|
+
} else {
|
|
898
|
+
merged.push({
|
|
899
|
+
type: "tool_executions",
|
|
900
|
+
executions: [...executions]
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return merged;
|
|
905
|
+
}
|
|
906
|
+
function mergePendingUser(chunks, pending, incoming) {
|
|
907
|
+
if (!pending) {
|
|
908
|
+
return incoming;
|
|
909
|
+
}
|
|
910
|
+
const sameText = isEquivalentUserContent(pending.content, incoming.content);
|
|
911
|
+
if (sameText) {
|
|
912
|
+
const preferred = pickPreferredEquivalentUserContent(pending, incoming);
|
|
913
|
+
return {
|
|
914
|
+
...preferred,
|
|
915
|
+
attachments: mergeAttachments(pending.attachments, incoming.attachments)
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
pushPendingUserChunk(chunks, pending);
|
|
919
|
+
return incoming;
|
|
920
|
+
}
|
|
921
|
+
function pushPendingUserChunk(chunks, pending) {
|
|
922
|
+
if (classifyCodexBootstrapMessage(pending.content)) {
|
|
923
|
+
chunks.push({
|
|
924
|
+
type: "system",
|
|
925
|
+
content: pending.content,
|
|
926
|
+
timestamp: pending.timestamp
|
|
927
|
+
});
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
const userChunk = {
|
|
931
|
+
type: "user",
|
|
932
|
+
content: pending.content,
|
|
933
|
+
timestamp: pending.timestamp
|
|
934
|
+
};
|
|
935
|
+
if (pending.attachments.length > 0) {
|
|
936
|
+
userChunk.attachments = pending.attachments;
|
|
937
|
+
}
|
|
938
|
+
chunks.push(userChunk);
|
|
939
|
+
}
|
|
940
|
+
function toUserPayload(entry) {
|
|
941
|
+
if (isResponseItemEntry(entry) && isMessagePayload(entry.payload) && entry.payload.role === "user") {
|
|
942
|
+
const attachments = extractAttachmentsFromResponseUser(entry, entry.timestamp);
|
|
943
|
+
let content = sanitizeUserText(entry.payload.content.map(getContentBlockText).filter(Boolean).join("\n"));
|
|
944
|
+
if (!content && attachments.length > 0) {
|
|
945
|
+
content = attachments.map((attachment, index) => attachmentPlaceholder(attachment, index)).join("\n");
|
|
946
|
+
}
|
|
947
|
+
return {
|
|
948
|
+
content,
|
|
949
|
+
attachments
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
if (isEventMsgEntry(entry) && isUserMessagePayload(entry.payload)) {
|
|
953
|
+
return {
|
|
954
|
+
content: sanitizeUserText(entry.payload.message),
|
|
955
|
+
attachments: []
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
return {
|
|
959
|
+
content: "",
|
|
960
|
+
attachments: []
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
function parseToolOutputError(output) {
|
|
964
|
+
try {
|
|
965
|
+
const parsed = JSON.parse(output);
|
|
966
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
const outputRecord = parsed;
|
|
970
|
+
if (typeof outputRecord.is_error === "boolean") {
|
|
971
|
+
return outputRecord.is_error;
|
|
972
|
+
}
|
|
973
|
+
if (typeof outputRecord.exit_code === "number") {
|
|
974
|
+
return outputRecord.exit_code !== 0;
|
|
975
|
+
}
|
|
976
|
+
const metadata = outputRecord.metadata;
|
|
977
|
+
if (typeof metadata === "object" && metadata !== null) {
|
|
978
|
+
const metadataRecord = metadata;
|
|
979
|
+
if (typeof metadataRecord.exit_code === "number") {
|
|
980
|
+
return metadataRecord.exit_code !== 0;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
} catch {
|
|
984
|
+
}
|
|
985
|
+
return false;
|
|
986
|
+
}
|
|
987
|
+
class CodexChunkBuilder {
|
|
988
|
+
constructor(classifier = new CodexMessageClassifier()) {
|
|
989
|
+
this.classifier = classifier;
|
|
990
|
+
}
|
|
991
|
+
buildChunks(entries) {
|
|
992
|
+
const sortedEntries = [...entries].sort(
|
|
993
|
+
(a, b) => parseTimestampMs$1(a.timestamp) - parseTimestampMs$1(b.timestamp)
|
|
994
|
+
);
|
|
995
|
+
const chunks = [];
|
|
996
|
+
let currentAI = null;
|
|
997
|
+
let pendingUser = null;
|
|
998
|
+
let lastSeenModelUsage = null;
|
|
999
|
+
let lastSeenCollaborationMode = "";
|
|
1000
|
+
const flushAIChunk = () => {
|
|
1001
|
+
if (!currentAI) {
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
const sections = buildOrderedAISections(currentAI);
|
|
1005
|
+
const textBlocks = [];
|
|
1006
|
+
const reasoning = [];
|
|
1007
|
+
for (const section of sections) {
|
|
1008
|
+
if (section.type === "message") {
|
|
1009
|
+
for (const block of section.textBlocks) {
|
|
1010
|
+
pushUniqueAdjacent(textBlocks, block);
|
|
1011
|
+
}
|
|
1012
|
+
} else if (section.type === "reasoning") {
|
|
1013
|
+
for (const summary of section.summaries) {
|
|
1014
|
+
pushUniqueAdjacent(reasoning, summary);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
if (textBlocks.length === 0 && reasoning.length === 0 && currentAI.toolExecutions.length === 0) {
|
|
1019
|
+
currentAI = null;
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
const aiChunk = {
|
|
1023
|
+
type: "ai",
|
|
1024
|
+
textBlocks,
|
|
1025
|
+
toolExecutions: currentAI.toolExecutions,
|
|
1026
|
+
reasoning,
|
|
1027
|
+
sections,
|
|
1028
|
+
metrics: {
|
|
1029
|
+
...currentAI.metrics,
|
|
1030
|
+
toolCallCount: currentAI.toolExecutions.length
|
|
1031
|
+
},
|
|
1032
|
+
timestamp: currentAI.timestamp,
|
|
1033
|
+
duration: Math.max(currentAI.endTimeMs - currentAI.startTimeMs, 0)
|
|
1034
|
+
};
|
|
1035
|
+
chunks.push(aiChunk);
|
|
1036
|
+
currentAI = null;
|
|
1037
|
+
};
|
|
1038
|
+
const ensureAIChunk = (timestamp) => {
|
|
1039
|
+
if (currentAI) {
|
|
1040
|
+
return currentAI;
|
|
1041
|
+
}
|
|
1042
|
+
const at = parseTimestampMs$1(timestamp);
|
|
1043
|
+
currentAI = {
|
|
1044
|
+
startTimeMs: at,
|
|
1045
|
+
endTimeMs: at,
|
|
1046
|
+
timestamp,
|
|
1047
|
+
responseTextBlocks: [],
|
|
1048
|
+
eventTextBlocks: [],
|
|
1049
|
+
toolExecutions: [],
|
|
1050
|
+
responseReasoning: [],
|
|
1051
|
+
eventReasoning: [],
|
|
1052
|
+
sections: [],
|
|
1053
|
+
metrics: {},
|
|
1054
|
+
callIndexById: /* @__PURE__ */ new Map(),
|
|
1055
|
+
pendingUsageToolIndex: null
|
|
1056
|
+
};
|
|
1057
|
+
return currentAI;
|
|
1058
|
+
};
|
|
1059
|
+
const flushPendingEventUser = () => {
|
|
1060
|
+
if (!pendingUser) {
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
pushPendingUserChunk(chunks, pendingUser);
|
|
1064
|
+
pendingUser = null;
|
|
1065
|
+
};
|
|
1066
|
+
for (const entry of sortedEntries) {
|
|
1067
|
+
const timestampMs = parseTimestampMs$1(entry.timestamp);
|
|
1068
|
+
if (isCompactedEntry(entry) || isCompactionEntry(entry)) {
|
|
1069
|
+
flushPendingEventUser();
|
|
1070
|
+
flushAIChunk();
|
|
1071
|
+
if (shouldInsertCompactionChunk(chunks, entry.timestamp)) {
|
|
1072
|
+
chunks.push({
|
|
1073
|
+
type: "compaction",
|
|
1074
|
+
timestamp: entry.timestamp
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
if (isTurnContextEntry(entry)) {
|
|
1080
|
+
const model = normalizeModel$2(entry.payload.model);
|
|
1081
|
+
if (model) {
|
|
1082
|
+
const usage = {
|
|
1083
|
+
model,
|
|
1084
|
+
reasoningEffort: normalizeReasoningEffort$2(entry.payload.effort)
|
|
1085
|
+
};
|
|
1086
|
+
if (lastSeenModelUsage === null) {
|
|
1087
|
+
lastSeenModelUsage = usage;
|
|
1088
|
+
} else if (lastSeenModelUsage.model !== usage.model || lastSeenModelUsage.reasoningEffort !== usage.reasoningEffort) {
|
|
1089
|
+
flushPendingEventUser();
|
|
1090
|
+
flushAIChunk();
|
|
1091
|
+
chunks.push({
|
|
1092
|
+
type: "model_change",
|
|
1093
|
+
previousModel: lastSeenModelUsage.model,
|
|
1094
|
+
previousReasoningEffort: lastSeenModelUsage.reasoningEffort,
|
|
1095
|
+
model: usage.model,
|
|
1096
|
+
reasoningEffort: usage.reasoningEffort,
|
|
1097
|
+
timestamp: entry.timestamp
|
|
1098
|
+
});
|
|
1099
|
+
lastSeenModelUsage = usage;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
const collaborationMode = normalizeCollaborationMode(entry.payload.collaboration_mode);
|
|
1103
|
+
if (collaborationMode) {
|
|
1104
|
+
if (!lastSeenCollaborationMode) {
|
|
1105
|
+
lastSeenCollaborationMode = collaborationMode;
|
|
1106
|
+
} else if (lastSeenCollaborationMode !== collaborationMode) {
|
|
1107
|
+
flushPendingEventUser();
|
|
1108
|
+
flushAIChunk();
|
|
1109
|
+
chunks.push({
|
|
1110
|
+
type: "collaboration_mode_change",
|
|
1111
|
+
previousMode: lastSeenCollaborationMode,
|
|
1112
|
+
mode: collaborationMode,
|
|
1113
|
+
timestamp: entry.timestamp
|
|
1114
|
+
});
|
|
1115
|
+
lastSeenCollaborationMode = collaborationMode;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
if (isEventMsgEntry(entry) && isContextCompactedPayload(entry.payload)) {
|
|
1121
|
+
flushPendingEventUser();
|
|
1122
|
+
flushAIChunk();
|
|
1123
|
+
if (shouldInsertCompactionChunk(chunks, entry.timestamp)) {
|
|
1124
|
+
chunks.push({
|
|
1125
|
+
type: "compaction",
|
|
1126
|
+
timestamp: entry.timestamp
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
if (isEventMsgEntry(entry) && isUserMessagePayload(entry.payload)) {
|
|
1132
|
+
flushAIChunk();
|
|
1133
|
+
const payload = toUserPayload(entry);
|
|
1134
|
+
const content = payload.content.trim();
|
|
1135
|
+
if (!content) {
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
pendingUser = mergePendingUser(chunks, pendingUser, {
|
|
1139
|
+
content,
|
|
1140
|
+
timestamp: entry.timestamp,
|
|
1141
|
+
source: "event",
|
|
1142
|
+
attachments: payload.attachments
|
|
1143
|
+
});
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
const isUser = this.classifier.isUserMessage(entry);
|
|
1147
|
+
if (isUser) {
|
|
1148
|
+
flushAIChunk();
|
|
1149
|
+
const payload = toUserPayload(entry);
|
|
1150
|
+
const content = payload.content.trim();
|
|
1151
|
+
if (!content) {
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
pendingUser = mergePendingUser(chunks, pendingUser, {
|
|
1155
|
+
content,
|
|
1156
|
+
timestamp: entry.timestamp,
|
|
1157
|
+
source: "response",
|
|
1158
|
+
attachments: payload.attachments
|
|
1159
|
+
});
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1162
|
+
flushPendingEventUser();
|
|
1163
|
+
if (isResponseItemEntry(entry) && isMessagePayload(entry.payload) && entry.payload.role === "developer") {
|
|
1164
|
+
flushAIChunk();
|
|
1165
|
+
chunks.push({
|
|
1166
|
+
type: "system",
|
|
1167
|
+
content: entry.payload.content.map(getContentBlockText).filter(Boolean).join("\n"),
|
|
1168
|
+
timestamp: entry.timestamp
|
|
1169
|
+
});
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
const ai = ensureAIChunk(entry.timestamp);
|
|
1173
|
+
ai.endTimeMs = Math.max(ai.endTimeMs, timestampMs);
|
|
1174
|
+
if (isResponseItemEntry(entry) && isMessagePayload(entry.payload) && entry.payload.role === "assistant") {
|
|
1175
|
+
const entryTextBlocks = [];
|
|
1176
|
+
for (const content of entry.payload.content) {
|
|
1177
|
+
const text = getContentBlockText(content);
|
|
1178
|
+
if (text) {
|
|
1179
|
+
pushUniqueAdjacent(ai.responseTextBlocks, text);
|
|
1180
|
+
pushUniqueAdjacent(entryTextBlocks, text);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
addMessageSection(ai, "response", entryTextBlocks);
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
if (isResponseItemEntry(entry) && isFunctionCallPayload(entry.payload)) {
|
|
1187
|
+
const toolExecution = {
|
|
1188
|
+
functionCall: {
|
|
1189
|
+
name: entry.payload.name,
|
|
1190
|
+
arguments: entry.payload.arguments,
|
|
1191
|
+
callId: entry.payload.call_id
|
|
1192
|
+
},
|
|
1193
|
+
functionOutput: null,
|
|
1194
|
+
duration: 0,
|
|
1195
|
+
tokenUsage: null
|
|
1196
|
+
};
|
|
1197
|
+
ai.pendingUsageToolIndex = ai.toolExecutions.length;
|
|
1198
|
+
ai.callIndexById.set(entry.payload.call_id, {
|
|
1199
|
+
index: ai.toolExecutions.length,
|
|
1200
|
+
startTimeMs: timestampMs
|
|
1201
|
+
});
|
|
1202
|
+
ai.toolExecutions.push(toolExecution);
|
|
1203
|
+
addToolSectionIndex(ai, ai.toolExecutions.length - 1);
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
if (isResponseItemEntry(entry) && isFunctionCallOutputPayload(entry.payload)) {
|
|
1207
|
+
const existing = ai.callIndexById.get(entry.payload.call_id);
|
|
1208
|
+
if (existing) {
|
|
1209
|
+
const tool = ai.toolExecutions[existing.index];
|
|
1210
|
+
tool.functionOutput = {
|
|
1211
|
+
callId: entry.payload.call_id,
|
|
1212
|
+
output: entry.payload.output,
|
|
1213
|
+
isError: parseToolOutputError(entry.payload.output)
|
|
1214
|
+
};
|
|
1215
|
+
tool.duration = Math.max(timestampMs - existing.startTimeMs, 0);
|
|
1216
|
+
addToolSectionIndex(ai, existing.index);
|
|
1217
|
+
} else {
|
|
1218
|
+
ai.toolExecutions.push({
|
|
1219
|
+
functionCall: {
|
|
1220
|
+
name: "unknown",
|
|
1221
|
+
arguments: "",
|
|
1222
|
+
callId: entry.payload.call_id
|
|
1223
|
+
},
|
|
1224
|
+
functionOutput: {
|
|
1225
|
+
callId: entry.payload.call_id,
|
|
1226
|
+
output: entry.payload.output,
|
|
1227
|
+
isError: parseToolOutputError(entry.payload.output)
|
|
1228
|
+
},
|
|
1229
|
+
duration: 0,
|
|
1230
|
+
tokenUsage: null
|
|
1231
|
+
});
|
|
1232
|
+
addToolSectionIndex(ai, ai.toolExecutions.length - 1);
|
|
1233
|
+
}
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
if (isResponseItemEntry(entry) && isReasoningPayload(entry.payload)) {
|
|
1237
|
+
const entrySummaries = reasoningSummaryToText(entry.payload.summary);
|
|
1238
|
+
for (const text of entrySummaries) {
|
|
1239
|
+
pushUniqueAdjacent(ai.responseReasoning, text);
|
|
1240
|
+
}
|
|
1241
|
+
addReasoningSection(ai, "response", entrySummaries);
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
if (isEventMsgEntry(entry) && isAgentMessagePayload(entry.payload)) {
|
|
1245
|
+
pushUniqueAdjacent(ai.eventTextBlocks, entry.payload.message);
|
|
1246
|
+
addMessageSection(ai, "event", [entry.payload.message]);
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
if (isEventMsgEntry(entry) && isAgentReasoningPayload(entry.payload)) {
|
|
1250
|
+
pushUniqueAdjacent(ai.eventReasoning, entry.payload.text);
|
|
1251
|
+
addReasoningSection(ai, "event", [entry.payload.text]);
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
if (isEventMsgEntry(entry) && isTokenCountPayload(entry.payload)) {
|
|
1255
|
+
this.accumulateMetricsFromTokenEvent(ai.metrics, entry);
|
|
1256
|
+
this.assignTokenUsageToPendingTool(ai, entry);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
flushPendingEventUser();
|
|
1260
|
+
flushAIChunk();
|
|
1261
|
+
return chunks;
|
|
1262
|
+
}
|
|
1263
|
+
accumulateMetricsFromTokenEvent(target, entry) {
|
|
1264
|
+
if (!isTokenCountPayload(entry.payload) || !entry.payload.info) {
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
const usage = entry.payload.info.last_token_usage;
|
|
1268
|
+
target.inputTokens = (target.inputTokens ?? 0) + usage.input_tokens;
|
|
1269
|
+
target.cachedTokens = (target.cachedTokens ?? 0) + usage.cached_input_tokens;
|
|
1270
|
+
target.outputTokens = (target.outputTokens ?? 0) + usage.output_tokens;
|
|
1271
|
+
target.reasoningTokens = (target.reasoningTokens ?? 0) + usage.reasoning_output_tokens;
|
|
1272
|
+
target.totalTokens = (target.totalTokens ?? 0) + usage.total_tokens;
|
|
1273
|
+
}
|
|
1274
|
+
assignTokenUsageToPendingTool(ai, entry) {
|
|
1275
|
+
if (!isTokenCountPayload(entry.payload) || !entry.payload.info) {
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
if (ai.pendingUsageToolIndex === null) {
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
const tool = ai.toolExecutions[ai.pendingUsageToolIndex];
|
|
1282
|
+
if (!tool) {
|
|
1283
|
+
ai.pendingUsageToolIndex = null;
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
const usage = entry.payload.info.last_token_usage;
|
|
1287
|
+
if (tool.tokenUsage) {
|
|
1288
|
+
tool.tokenUsage.inputTokens += usage.input_tokens;
|
|
1289
|
+
tool.tokenUsage.outputTokens += usage.output_tokens;
|
|
1290
|
+
} else {
|
|
1291
|
+
tool.tokenUsage = {
|
|
1292
|
+
inputTokens: usage.input_tokens,
|
|
1293
|
+
outputTokens: usage.output_tokens
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
ai.pendingUsageToolIndex = null;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
const logger$2 = createLogger("Main:jsonl");
|
|
1300
|
+
const defaultParser = (value) => value;
|
|
1301
|
+
function waitForStreamClose(stream) {
|
|
1302
|
+
if (stream.closed) {
|
|
1303
|
+
return Promise.resolve();
|
|
1304
|
+
}
|
|
1305
|
+
return new Promise((resolve) => {
|
|
1306
|
+
const done = () => {
|
|
1307
|
+
stream.off("close", done);
|
|
1308
|
+
stream.off("error", done);
|
|
1309
|
+
resolve();
|
|
1310
|
+
};
|
|
1311
|
+
stream.once("close", done);
|
|
1312
|
+
stream.once("error", done);
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
async function closeReaderAndStream(reader, stream) {
|
|
1316
|
+
const closePromise = waitForStreamClose(stream);
|
|
1317
|
+
reader.close();
|
|
1318
|
+
if (!stream.destroyed) {
|
|
1319
|
+
stream.destroy();
|
|
1320
|
+
}
|
|
1321
|
+
await closePromise;
|
|
1322
|
+
}
|
|
1323
|
+
async function streamJsonlFile(filePath, options) {
|
|
1324
|
+
const parser = options.parser ?? defaultParser;
|
|
1325
|
+
const stream = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
1326
|
+
const reader = readline__namespace.createInterface({
|
|
1327
|
+
input: stream,
|
|
1328
|
+
crlfDelay: Infinity
|
|
1329
|
+
});
|
|
1330
|
+
let lineNumber = 0;
|
|
1331
|
+
try {
|
|
1332
|
+
for await (const line of reader) {
|
|
1333
|
+
lineNumber += 1;
|
|
1334
|
+
if (!line.trim()) {
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
try {
|
|
1338
|
+
const raw = JSON.parse(line);
|
|
1339
|
+
const parsed = parser(raw, lineNumber);
|
|
1340
|
+
if (parsed !== null) {
|
|
1341
|
+
await options.onEntry(parsed, lineNumber);
|
|
1342
|
+
}
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
options.onError?.(error, line, lineNumber);
|
|
1345
|
+
if (!options.onError) {
|
|
1346
|
+
logger$2.warn(`Failed to parse JSONL line ${lineNumber} in ${filePath}`, error);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
} finally {
|
|
1351
|
+
await closeReaderAndStream(reader, stream);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
async function parseJsonlFile(filePath, parser) {
|
|
1355
|
+
const entries = [];
|
|
1356
|
+
await streamJsonlFile(filePath, {
|
|
1357
|
+
parser,
|
|
1358
|
+
onEntry: (entry) => {
|
|
1359
|
+
entries.push(entry);
|
|
1360
|
+
}
|
|
1361
|
+
});
|
|
1362
|
+
return entries;
|
|
1363
|
+
}
|
|
1364
|
+
async function readFirstJsonlEntry(filePath, parser) {
|
|
1365
|
+
const entryParser = parser ?? defaultParser;
|
|
1366
|
+
const stream = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
1367
|
+
const reader = readline__namespace.createInterface({
|
|
1368
|
+
input: stream,
|
|
1369
|
+
crlfDelay: Infinity
|
|
1370
|
+
});
|
|
1371
|
+
let lineNumber = 0;
|
|
1372
|
+
let firstEntry = null;
|
|
1373
|
+
try {
|
|
1374
|
+
for await (const line of reader) {
|
|
1375
|
+
lineNumber += 1;
|
|
1376
|
+
if (!line.trim()) {
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
try {
|
|
1380
|
+
const raw = JSON.parse(line);
|
|
1381
|
+
const parsed = entryParser(raw, lineNumber);
|
|
1382
|
+
if (parsed !== null) {
|
|
1383
|
+
firstEntry = parsed;
|
|
1384
|
+
break;
|
|
1385
|
+
}
|
|
1386
|
+
} catch (error) {
|
|
1387
|
+
logger$2.warn(`Failed to parse JSONL line ${lineNumber} in ${filePath}`, error);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
} finally {
|
|
1391
|
+
await closeReaderAndStream(reader, stream);
|
|
1392
|
+
}
|
|
1393
|
+
return firstEntry;
|
|
1394
|
+
}
|
|
1395
|
+
const logger$1 = createLogger("Service:CodexSessionScanner");
|
|
1396
|
+
function isWithinDateRange(timestamp, options) {
|
|
1397
|
+
const current = new Date(timestamp).getTime();
|
|
1398
|
+
if (Number.isNaN(current)) {
|
|
1399
|
+
return false;
|
|
1400
|
+
}
|
|
1401
|
+
if (options.startDate && current < options.startDate.getTime()) {
|
|
1402
|
+
return false;
|
|
1403
|
+
}
|
|
1404
|
+
if (options.endDate && current > options.endDate.getTime()) {
|
|
1405
|
+
return false;
|
|
1406
|
+
}
|
|
1407
|
+
return true;
|
|
1408
|
+
}
|
|
1409
|
+
function fallbackSessionIdFromName(fileName) {
|
|
1410
|
+
const match = fileName.match(/-([a-zA-Z0-9-]+)\.jsonl$/);
|
|
1411
|
+
return match?.[1] ?? fileName.replace(/\.jsonl$/, "");
|
|
1412
|
+
}
|
|
1413
|
+
function normalizeModel$1(model) {
|
|
1414
|
+
return model?.trim() ?? "";
|
|
1415
|
+
}
|
|
1416
|
+
function normalizeReasoningEffort$1(effort) {
|
|
1417
|
+
const value = effort?.trim();
|
|
1418
|
+
return value ? value : "unknown";
|
|
1419
|
+
}
|
|
1420
|
+
async function getFileSizeBytes(filePath) {
|
|
1421
|
+
try {
|
|
1422
|
+
const stats = await fs.promises.stat(filePath);
|
|
1423
|
+
return stats.isFile() ? stats.size : void 0;
|
|
1424
|
+
} catch {
|
|
1425
|
+
return void 0;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
class CodexSessionScanner {
|
|
1429
|
+
constructor(sessionsRoot = path__namespace.join(os__namespace.homedir(), ".codex", "sessions")) {
|
|
1430
|
+
this.sessionsRoot = sessionsRoot;
|
|
1431
|
+
}
|
|
1432
|
+
async scanSessions(options = {}) {
|
|
1433
|
+
const sessionFiles = await this.findRolloutFiles(this.sessionsRoot);
|
|
1434
|
+
const sessions = [];
|
|
1435
|
+
for (const filePath of sessionFiles) {
|
|
1436
|
+
const metaEntry = await readFirstJsonlEntry(
|
|
1437
|
+
filePath,
|
|
1438
|
+
(value) => isSessionMetaEntry(value) ? value : null
|
|
1439
|
+
);
|
|
1440
|
+
if (!metaEntry) {
|
|
1441
|
+
logger$1.warn(`Skipping session file without valid session_meta: ${filePath}`);
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
if (!isWithinDateRange(metaEntry.timestamp, options)) {
|
|
1445
|
+
continue;
|
|
1446
|
+
}
|
|
1447
|
+
const [firstTurnUsage, fileSizeBytes] = await Promise.all([
|
|
1448
|
+
readFirstJsonlEntry(filePath, (value) => {
|
|
1449
|
+
if (!isTurnContextEntry(value)) {
|
|
1450
|
+
return null;
|
|
1451
|
+
}
|
|
1452
|
+
const model = normalizeModel$1(value.payload.model);
|
|
1453
|
+
if (!model) {
|
|
1454
|
+
return null;
|
|
1455
|
+
}
|
|
1456
|
+
return {
|
|
1457
|
+
model,
|
|
1458
|
+
reasoningEffort: normalizeReasoningEffort$1(value.payload.effort)
|
|
1459
|
+
};
|
|
1460
|
+
}),
|
|
1461
|
+
getFileSizeBytes(filePath)
|
|
1462
|
+
]);
|
|
1463
|
+
const sessionMetaModel = normalizeModel$1(metaEntry.payload.model);
|
|
1464
|
+
const firstTurnModel = firstTurnUsage?.model ?? "";
|
|
1465
|
+
const modelUsages = firstTurnUsage ? [firstTurnUsage] : sessionMetaModel ? [{ model: sessionMetaModel, reasoningEffort: "unknown" }] : [];
|
|
1466
|
+
const fileName = path__namespace.basename(filePath);
|
|
1467
|
+
sessions.push({
|
|
1468
|
+
id: metaEntry.payload.id ?? fallbackSessionIdFromName(fileName),
|
|
1469
|
+
filePath,
|
|
1470
|
+
fileSizeBytes,
|
|
1471
|
+
cwd: metaEntry.payload.cwd ?? "",
|
|
1472
|
+
model: firstTurnModel || sessionMetaModel,
|
|
1473
|
+
modelUsages,
|
|
1474
|
+
cliVersion: metaEntry.payload.cli_version ?? "",
|
|
1475
|
+
gitBranch: metaEntry.payload.git?.branch ?? "",
|
|
1476
|
+
gitCommit: metaEntry.payload.git?.commit_hash ?? "",
|
|
1477
|
+
startTime: metaEntry.timestamp,
|
|
1478
|
+
modelProvider: metaEntry.payload.model_provider ?? ""
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
sessions.sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime());
|
|
1482
|
+
return sessions;
|
|
1483
|
+
}
|
|
1484
|
+
async scanProjects(options = {}) {
|
|
1485
|
+
const sessions = await this.scanSessions(options);
|
|
1486
|
+
const projectMap = /* @__PURE__ */ new Map();
|
|
1487
|
+
for (const session of sessions) {
|
|
1488
|
+
const existing = projectMap.get(session.cwd);
|
|
1489
|
+
if (!existing) {
|
|
1490
|
+
projectMap.set(session.cwd, {
|
|
1491
|
+
cwd: session.cwd,
|
|
1492
|
+
name: path__namespace.basename(session.cwd) || session.cwd,
|
|
1493
|
+
sessionCount: 1,
|
|
1494
|
+
lastActivity: session.startTime
|
|
1495
|
+
});
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
existing.sessionCount += 1;
|
|
1499
|
+
if (new Date(session.startTime).getTime() > new Date(existing.lastActivity).getTime()) {
|
|
1500
|
+
existing.lastActivity = session.startTime;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
return Array.from(projectMap.values()).sort(
|
|
1504
|
+
(a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime()
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
async findRolloutFiles(root) {
|
|
1508
|
+
try {
|
|
1509
|
+
const rootStat = await fs.promises.stat(root);
|
|
1510
|
+
if (!rootStat.isDirectory()) {
|
|
1511
|
+
return [];
|
|
1512
|
+
}
|
|
1513
|
+
} catch {
|
|
1514
|
+
return [];
|
|
1515
|
+
}
|
|
1516
|
+
const files = [];
|
|
1517
|
+
const pending = [root];
|
|
1518
|
+
while (pending.length > 0) {
|
|
1519
|
+
const current = pending.pop();
|
|
1520
|
+
if (!current) {
|
|
1521
|
+
continue;
|
|
1522
|
+
}
|
|
1523
|
+
const entries = await fs.promises.readdir(current, { withFileTypes: true });
|
|
1524
|
+
for (const entry of entries) {
|
|
1525
|
+
const fullPath = path__namespace.join(current, entry.name);
|
|
1526
|
+
if (entry.isDirectory()) {
|
|
1527
|
+
pending.push(fullPath);
|
|
1528
|
+
continue;
|
|
1529
|
+
}
|
|
1530
|
+
if (entry.isFile() && /^rollout-.*\.jsonl$/.test(entry.name)) {
|
|
1531
|
+
files.push(fullPath);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
return files;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
function coerceTimestamp(value, fallback) {
|
|
1539
|
+
if (!value) {
|
|
1540
|
+
return fallback;
|
|
1541
|
+
}
|
|
1542
|
+
const date = new Date(value);
|
|
1543
|
+
return Number.isNaN(date.getTime()) ? fallback : value;
|
|
1544
|
+
}
|
|
1545
|
+
function parseTimestampMs(value) {
|
|
1546
|
+
const date = new Date(value);
|
|
1547
|
+
return Number.isNaN(date.getTime()) ? 0 : date.getTime();
|
|
1548
|
+
}
|
|
1549
|
+
function fallbackSessionIdFromFilePath(filePath) {
|
|
1550
|
+
const fileName = path__namespace.basename(filePath);
|
|
1551
|
+
const match = fileName.match(/-([a-zA-Z0-9-]+)\.jsonl$/);
|
|
1552
|
+
return match?.[1] ?? fileName.replace(/\.jsonl$/, "");
|
|
1553
|
+
}
|
|
1554
|
+
function normalizeModel(model) {
|
|
1555
|
+
return model?.trim() ?? "";
|
|
1556
|
+
}
|
|
1557
|
+
function normalizeReasoningEffort(effort) {
|
|
1558
|
+
const value = effort?.trim();
|
|
1559
|
+
return value ? value : "unknown";
|
|
1560
|
+
}
|
|
1561
|
+
class CodexSessionParser {
|
|
1562
|
+
constructor(classifier = new CodexMessageClassifier()) {
|
|
1563
|
+
this.classifier = classifier;
|
|
1564
|
+
}
|
|
1565
|
+
async parseSessionFile(filePath) {
|
|
1566
|
+
const entries = await parseJsonlFile(
|
|
1567
|
+
filePath,
|
|
1568
|
+
(value) => isCodexLogEntry(value) ? value : null
|
|
1569
|
+
);
|
|
1570
|
+
const responseItems = [];
|
|
1571
|
+
const turnContexts = [];
|
|
1572
|
+
const eventMessages = [];
|
|
1573
|
+
let sessionMeta = null;
|
|
1574
|
+
const metrics = { ...EMPTY_CODEX_SESSION_METRICS };
|
|
1575
|
+
let firstTimestamp = null;
|
|
1576
|
+
let lastTimestamp = null;
|
|
1577
|
+
let firstTurnContextModel = null;
|
|
1578
|
+
const modelUsages = [];
|
|
1579
|
+
const modelUsageKeys = /* @__PURE__ */ new Set();
|
|
1580
|
+
for (const entry of entries) {
|
|
1581
|
+
if (firstTimestamp === null) {
|
|
1582
|
+
firstTimestamp = entry.timestamp;
|
|
1583
|
+
}
|
|
1584
|
+
lastTimestamp = entry.timestamp;
|
|
1585
|
+
if (isSessionMetaEntry(entry) && sessionMeta === null) {
|
|
1586
|
+
sessionMeta = entry;
|
|
1587
|
+
continue;
|
|
1588
|
+
}
|
|
1589
|
+
if (isResponseItemEntry(entry)) {
|
|
1590
|
+
responseItems.push(entry);
|
|
1591
|
+
if (entry.payload.type === "function_call") {
|
|
1592
|
+
metrics.toolCallCount += 1;
|
|
1593
|
+
}
|
|
1594
|
+
continue;
|
|
1595
|
+
}
|
|
1596
|
+
if (isTurnContextEntry(entry)) {
|
|
1597
|
+
turnContexts.push(entry);
|
|
1598
|
+
const model = normalizeModel(entry.payload.model);
|
|
1599
|
+
if (firstTurnContextModel === null && model) {
|
|
1600
|
+
firstTurnContextModel = model;
|
|
1601
|
+
}
|
|
1602
|
+
if (model) {
|
|
1603
|
+
const usage = {
|
|
1604
|
+
model,
|
|
1605
|
+
reasoningEffort: normalizeReasoningEffort(entry.payload.effort)
|
|
1606
|
+
};
|
|
1607
|
+
const usageKey = `${usage.model}::${usage.reasoningEffort}`;
|
|
1608
|
+
if (!modelUsageKeys.has(usageKey)) {
|
|
1609
|
+
modelUsageKeys.add(usageKey);
|
|
1610
|
+
modelUsages.push(usage);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
continue;
|
|
1614
|
+
}
|
|
1615
|
+
if (isEventMsgEntry(entry)) {
|
|
1616
|
+
eventMessages.push(entry);
|
|
1617
|
+
if (isTokenCountPayload(entry.payload) && entry.payload.info) {
|
|
1618
|
+
const usage = entry.payload.info.last_token_usage;
|
|
1619
|
+
metrics.inputTokens += usage.input_tokens;
|
|
1620
|
+
metrics.cachedTokens += usage.cached_input_tokens;
|
|
1621
|
+
metrics.outputTokens += usage.output_tokens;
|
|
1622
|
+
metrics.reasoningTokens += usage.reasoning_output_tokens;
|
|
1623
|
+
metrics.totalTokens += usage.total_tokens;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
const classifiedEntries = this.classifier.classifyEntries(entries);
|
|
1628
|
+
metrics.turnCount = classifiedEntries.filter((entry) => entry.kind === "user").length;
|
|
1629
|
+
if (firstTimestamp && lastTimestamp) {
|
|
1630
|
+
metrics.duration = Math.max(parseTimestampMs(lastTimestamp) - parseTimestampMs(firstTimestamp), 0);
|
|
1631
|
+
}
|
|
1632
|
+
const fallbackStart = firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
1633
|
+
const startTime = coerceTimestamp(sessionMeta?.timestamp, fallbackStart);
|
|
1634
|
+
const sessionMetaModel = normalizeModel(sessionMeta?.payload.model);
|
|
1635
|
+
if (sessionMetaModel && modelUsages.length === 0) {
|
|
1636
|
+
modelUsages.push({
|
|
1637
|
+
model: sessionMetaModel,
|
|
1638
|
+
reasoningEffort: "unknown"
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
const session = {
|
|
1642
|
+
id: sessionMeta?.payload.id ?? fallbackSessionIdFromFilePath(filePath),
|
|
1643
|
+
filePath,
|
|
1644
|
+
cwd: sessionMeta?.payload.cwd ?? turnContexts[0]?.payload.cwd ?? "",
|
|
1645
|
+
model: firstTurnContextModel ?? sessionMetaModel,
|
|
1646
|
+
modelUsages,
|
|
1647
|
+
cliVersion: sessionMeta?.payload.cli_version ?? "",
|
|
1648
|
+
gitBranch: sessionMeta?.payload.git?.branch ?? "",
|
|
1649
|
+
gitCommit: sessionMeta?.payload.git?.commit_hash ?? "",
|
|
1650
|
+
startTime,
|
|
1651
|
+
modelProvider: sessionMeta?.payload.model_provider ?? ""
|
|
1652
|
+
};
|
|
1653
|
+
return {
|
|
1654
|
+
filePath,
|
|
1655
|
+
entries,
|
|
1656
|
+
sessionMeta,
|
|
1657
|
+
responseItems,
|
|
1658
|
+
turnContexts,
|
|
1659
|
+
eventMessages,
|
|
1660
|
+
classifiedEntries,
|
|
1661
|
+
session,
|
|
1662
|
+
metrics
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
class DataCache {
|
|
1667
|
+
constructor(maxSize = 50, ttlMinutes = 10) {
|
|
1668
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
1669
|
+
this.maxSize = maxSize;
|
|
1670
|
+
this.ttlMs = ttlMinutes * 60 * 1e3;
|
|
1671
|
+
}
|
|
1672
|
+
get(key) {
|
|
1673
|
+
const entry = this.cache.get(key);
|
|
1674
|
+
if (!entry) {
|
|
1675
|
+
return void 0;
|
|
1676
|
+
}
|
|
1677
|
+
if (Date.now() - entry.timestamp > this.ttlMs) {
|
|
1678
|
+
this.cache.delete(key);
|
|
1679
|
+
return void 0;
|
|
1680
|
+
}
|
|
1681
|
+
this.cache.delete(key);
|
|
1682
|
+
this.cache.set(key, entry);
|
|
1683
|
+
return entry.value;
|
|
1684
|
+
}
|
|
1685
|
+
set(key, value) {
|
|
1686
|
+
if (this.cache.has(key)) {
|
|
1687
|
+
this.cache.delete(key);
|
|
1688
|
+
}
|
|
1689
|
+
this.cache.set(key, {
|
|
1690
|
+
value,
|
|
1691
|
+
timestamp: Date.now()
|
|
1692
|
+
});
|
|
1693
|
+
if (this.cache.size > this.maxSize) {
|
|
1694
|
+
const oldestKey = this.cache.keys().next().value;
|
|
1695
|
+
if (oldestKey) {
|
|
1696
|
+
this.cache.delete(oldestKey);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
has(key) {
|
|
1701
|
+
return this.get(key) !== void 0;
|
|
1702
|
+
}
|
|
1703
|
+
invalidate(key) {
|
|
1704
|
+
this.cache.delete(key);
|
|
1705
|
+
}
|
|
1706
|
+
clear() {
|
|
1707
|
+
this.cache.clear();
|
|
1708
|
+
}
|
|
1709
|
+
size() {
|
|
1710
|
+
return this.cache.size;
|
|
1711
|
+
}
|
|
1712
|
+
static buildKey(cwdHash, sessionId) {
|
|
1713
|
+
return `${cwdHash}/${sessionId}`;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
const logger = createLogger("Service:FileWatcher");
|
|
1717
|
+
const DEBOUNCE_MS = 100;
|
|
1718
|
+
class FileWatcher extends node_events.EventEmitter {
|
|
1719
|
+
constructor(sessionsPath = path__namespace.join(os__namespace.homedir(), ".codex", "sessions")) {
|
|
1720
|
+
super();
|
|
1721
|
+
this.watcher = null;
|
|
1722
|
+
this.debounceTimers = /* @__PURE__ */ new Map();
|
|
1723
|
+
this.sessionsPath = sessionsPath;
|
|
1724
|
+
}
|
|
1725
|
+
start() {
|
|
1726
|
+
if (this.watcher) {
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
try {
|
|
1730
|
+
this.watcher = fs__namespace.watch(
|
|
1731
|
+
this.sessionsPath,
|
|
1732
|
+
{ recursive: true },
|
|
1733
|
+
(eventType, filename) => {
|
|
1734
|
+
if (!filename) {
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
const targetPath = path__namespace.join(this.sessionsPath, String(filename));
|
|
1738
|
+
this.handleFsEvent(eventType, targetPath);
|
|
1739
|
+
}
|
|
1740
|
+
);
|
|
1741
|
+
} catch (error) {
|
|
1742
|
+
logger.error(`Failed to start watcher for ${this.sessionsPath}`, error);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
stop() {
|
|
1746
|
+
if (this.watcher) {
|
|
1747
|
+
this.watcher.close();
|
|
1748
|
+
this.watcher = null;
|
|
1749
|
+
}
|
|
1750
|
+
for (const timer of this.debounceTimers.values()) {
|
|
1751
|
+
clearTimeout(timer);
|
|
1752
|
+
}
|
|
1753
|
+
this.debounceTimers.clear();
|
|
1754
|
+
}
|
|
1755
|
+
dispose() {
|
|
1756
|
+
this.stop();
|
|
1757
|
+
this.removeAllListeners();
|
|
1758
|
+
}
|
|
1759
|
+
onFileChange(listener) {
|
|
1760
|
+
this.on("file-change", listener);
|
|
1761
|
+
return () => this.off("file-change", listener);
|
|
1762
|
+
}
|
|
1763
|
+
handleFsEvent(rawEventType, targetPath) {
|
|
1764
|
+
const baseName = path__namespace.basename(targetPath);
|
|
1765
|
+
if (!/^rollout-.*\.jsonl$/.test(baseName)) {
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
const existingTimer = this.debounceTimers.get(targetPath);
|
|
1769
|
+
if (existingTimer) {
|
|
1770
|
+
clearTimeout(existingTimer);
|
|
1771
|
+
}
|
|
1772
|
+
const timer = setTimeout(() => {
|
|
1773
|
+
this.debounceTimers.delete(targetPath);
|
|
1774
|
+
const event = {
|
|
1775
|
+
filePath: targetPath,
|
|
1776
|
+
eventType: this.resolveEventType(rawEventType, targetPath),
|
|
1777
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1778
|
+
};
|
|
1779
|
+
this.emit("file-change", event);
|
|
1780
|
+
}, DEBOUNCE_MS);
|
|
1781
|
+
this.debounceTimers.set(targetPath, timer);
|
|
1782
|
+
}
|
|
1783
|
+
resolveEventType(rawEventType, targetPath) {
|
|
1784
|
+
if (rawEventType !== "rename") {
|
|
1785
|
+
return "changed";
|
|
1786
|
+
}
|
|
1787
|
+
return fs__namespace.existsSync(targetPath) ? "created" : "deleted";
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
const DETAIL_CACHE_PREFIX = "detail-v2";
|
|
1791
|
+
const CHUNKS_CACHE_PREFIX = "chunks-v2";
|
|
1792
|
+
const SESSIONS_CACHE_PREFIX = "sessions";
|
|
1793
|
+
const UNKNOWN_REVISION = "unknown-revision";
|
|
1794
|
+
function hasCompactionSignals(entries) {
|
|
1795
|
+
return entries.some(
|
|
1796
|
+
(entry) => isCompactedEntry(entry) || isCompactionEntry(entry) || isEventMsgEntry(entry) && isContextCompactedPayload(entry.payload)
|
|
1797
|
+
);
|
|
1798
|
+
}
|
|
1799
|
+
function hasCompactionChunk(chunks) {
|
|
1800
|
+
return chunks.some((chunk) => chunk.type === "compaction");
|
|
1801
|
+
}
|
|
1802
|
+
function extractSearchContent(entry) {
|
|
1803
|
+
if (isResponseItemEntry(entry) && isMessagePayload(entry.payload)) {
|
|
1804
|
+
return entry.payload.content.map(getContentBlockText).filter(Boolean).join("\n").trim();
|
|
1805
|
+
}
|
|
1806
|
+
if (isResponseItemEntry(entry) && isFunctionCallPayload(entry.payload)) {
|
|
1807
|
+
return `${entry.payload.name}
|
|
1808
|
+
${entry.payload.arguments}`.trim();
|
|
1809
|
+
}
|
|
1810
|
+
if (isResponseItemEntry(entry) && isFunctionCallOutputPayload(entry.payload)) {
|
|
1811
|
+
return entry.payload.output.trim();
|
|
1812
|
+
}
|
|
1813
|
+
if (isResponseItemEntry(entry) && isReasoningPayload(entry.payload)) {
|
|
1814
|
+
return reasoningSummaryToText(entry.payload.summary).join("\n").trim();
|
|
1815
|
+
}
|
|
1816
|
+
if (isEventMsgEntry(entry) && isUserMessagePayload(entry.payload)) {
|
|
1817
|
+
return entry.payload.message.trim();
|
|
1818
|
+
}
|
|
1819
|
+
if (isEventMsgEntry(entry) && isAgentMessagePayload(entry.payload)) {
|
|
1820
|
+
return entry.payload.message.trim();
|
|
1821
|
+
}
|
|
1822
|
+
if (isEventMsgEntry(entry) && isAgentReasoningPayload(entry.payload)) {
|
|
1823
|
+
return entry.payload.text.trim();
|
|
1824
|
+
}
|
|
1825
|
+
return "";
|
|
1826
|
+
}
|
|
1827
|
+
function buildSnippet(content, query) {
|
|
1828
|
+
const normalizedContent = content.replace(/\s+/g, " ").trim();
|
|
1829
|
+
const lowercaseContent = normalizedContent.toLowerCase();
|
|
1830
|
+
const index = lowercaseContent.indexOf(query);
|
|
1831
|
+
if (index < 0) {
|
|
1832
|
+
return normalizedContent.slice(0, 180);
|
|
1833
|
+
}
|
|
1834
|
+
const start = Math.max(0, index - 60);
|
|
1835
|
+
const end = Math.min(normalizedContent.length, index + query.length + 120);
|
|
1836
|
+
return normalizedContent.slice(start, end);
|
|
1837
|
+
}
|
|
1838
|
+
function buildProjectsFromSessions(sessions) {
|
|
1839
|
+
const projectMap = /* @__PURE__ */ new Map();
|
|
1840
|
+
for (const session of sessions) {
|
|
1841
|
+
const existing = projectMap.get(session.cwd);
|
|
1842
|
+
if (!existing) {
|
|
1843
|
+
projectMap.set(session.cwd, {
|
|
1844
|
+
cwd: session.cwd,
|
|
1845
|
+
name: path__namespace.basename(session.cwd) || session.cwd,
|
|
1846
|
+
sessionCount: 1,
|
|
1847
|
+
lastActivity: session.startTime
|
|
1848
|
+
});
|
|
1849
|
+
continue;
|
|
1850
|
+
}
|
|
1851
|
+
existing.sessionCount += 1;
|
|
1852
|
+
if (new Date(session.startTime).getTime() > new Date(existing.lastActivity).getTime()) {
|
|
1853
|
+
existing.lastActivity = session.startTime;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
return Array.from(projectMap.values()).sort(
|
|
1857
|
+
(a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime()
|
|
1858
|
+
);
|
|
1859
|
+
}
|
|
1860
|
+
class CodexServiceContext {
|
|
1861
|
+
constructor(options = {}) {
|
|
1862
|
+
const sessionsPath = options.sessionsPath ?? process.env.CODEX_SESSIONS_PATH;
|
|
1863
|
+
this.scanner = new CodexSessionScanner(sessionsPath);
|
|
1864
|
+
this.parser = new CodexSessionParser();
|
|
1865
|
+
this.chunkBuilder = new CodexChunkBuilder();
|
|
1866
|
+
this.dataCache = new DataCache(options.cacheSize ?? 200, options.cacheTtlMinutes ?? 10);
|
|
1867
|
+
this.watcher = new FileWatcher(sessionsPath);
|
|
1868
|
+
this.configManager = new ConfigManager(options.configPath);
|
|
1869
|
+
this.removeFileChangeListener = this.watcher.onFileChange(() => {
|
|
1870
|
+
this.dataCache.clear();
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
start() {
|
|
1874
|
+
this.watcher.start();
|
|
1875
|
+
}
|
|
1876
|
+
stop() {
|
|
1877
|
+
this.watcher.stop();
|
|
1878
|
+
}
|
|
1879
|
+
dispose() {
|
|
1880
|
+
this.removeFileChangeListener();
|
|
1881
|
+
this.watcher.dispose();
|
|
1882
|
+
this.dataCache.clear();
|
|
1883
|
+
}
|
|
1884
|
+
onFileChange(listener) {
|
|
1885
|
+
return this.watcher.onFileChange(listener);
|
|
1886
|
+
}
|
|
1887
|
+
async getProjects() {
|
|
1888
|
+
const sessions = await this.getAllSessions();
|
|
1889
|
+
return buildProjectsFromSessions(sessions);
|
|
1890
|
+
}
|
|
1891
|
+
async getSessions(projectCwd) {
|
|
1892
|
+
if (!projectCwd) {
|
|
1893
|
+
return [];
|
|
1894
|
+
}
|
|
1895
|
+
const sessions = await this.getAllSessions();
|
|
1896
|
+
return sessions.filter((session) => session.cwd === projectCwd);
|
|
1897
|
+
}
|
|
1898
|
+
async getSessionDetail(sessionId) {
|
|
1899
|
+
if (!sessionId) {
|
|
1900
|
+
return null;
|
|
1901
|
+
}
|
|
1902
|
+
const session = await this.findSessionById(sessionId);
|
|
1903
|
+
if (!session) {
|
|
1904
|
+
return null;
|
|
1905
|
+
}
|
|
1906
|
+
const revision = await this.getSessionFileRevision(session.filePath);
|
|
1907
|
+
const cacheKey = DataCache.buildKey(DETAIL_CACHE_PREFIX, `${sessionId}:${revision}`);
|
|
1908
|
+
const cached = this.dataCache.get(cacheKey);
|
|
1909
|
+
if (cached) {
|
|
1910
|
+
return cached;
|
|
1911
|
+
}
|
|
1912
|
+
const parsed = await this.parser.parseSessionFile(session.filePath);
|
|
1913
|
+
this.dataCache.set(cacheKey, parsed);
|
|
1914
|
+
return parsed;
|
|
1915
|
+
}
|
|
1916
|
+
async getSessionChunks(sessionId) {
|
|
1917
|
+
if (!sessionId) {
|
|
1918
|
+
return null;
|
|
1919
|
+
}
|
|
1920
|
+
const session = await this.findSessionById(sessionId);
|
|
1921
|
+
if (!session) {
|
|
1922
|
+
return null;
|
|
1923
|
+
}
|
|
1924
|
+
const revision = await this.getSessionFileRevision(session.filePath);
|
|
1925
|
+
const cacheKey = DataCache.buildKey(CHUNKS_CACHE_PREFIX, `${sessionId}:${revision}`);
|
|
1926
|
+
const cached = this.dataCache.get(cacheKey);
|
|
1927
|
+
if (cached) {
|
|
1928
|
+
const detailForValidation = await this.getSessionDetail(sessionId);
|
|
1929
|
+
if (!detailForValidation) {
|
|
1930
|
+
return cached;
|
|
1931
|
+
}
|
|
1932
|
+
if (!hasCompactionSignals(detailForValidation.entries) || hasCompactionChunk(cached)) {
|
|
1933
|
+
return cached;
|
|
1934
|
+
}
|
|
1935
|
+
const refreshed = this.chunkBuilder.buildChunks(detailForValidation.entries);
|
|
1936
|
+
this.dataCache.set(cacheKey, refreshed);
|
|
1937
|
+
return refreshed;
|
|
1938
|
+
}
|
|
1939
|
+
const detail = await this.getSessionDetail(sessionId);
|
|
1940
|
+
if (!detail) {
|
|
1941
|
+
return null;
|
|
1942
|
+
}
|
|
1943
|
+
const chunks = this.chunkBuilder.buildChunks(detail.entries);
|
|
1944
|
+
this.dataCache.set(cacheKey, chunks);
|
|
1945
|
+
return chunks;
|
|
1946
|
+
}
|
|
1947
|
+
async searchSessions(query) {
|
|
1948
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
1949
|
+
if (!normalizedQuery) {
|
|
1950
|
+
return {
|
|
1951
|
+
query,
|
|
1952
|
+
totalMatches: 0,
|
|
1953
|
+
sessionsSearched: 0,
|
|
1954
|
+
results: []
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
const sessions = await this.getAllSessions();
|
|
1958
|
+
const results = [];
|
|
1959
|
+
for (const session of sessions) {
|
|
1960
|
+
const detail = await this.getSessionDetail(session.id);
|
|
1961
|
+
if (!detail) {
|
|
1962
|
+
continue;
|
|
1963
|
+
}
|
|
1964
|
+
for (const classified of detail.classifiedEntries) {
|
|
1965
|
+
const content = extractSearchContent(classified.entry);
|
|
1966
|
+
if (!content || !content.toLowerCase().includes(normalizedQuery)) {
|
|
1967
|
+
continue;
|
|
1968
|
+
}
|
|
1969
|
+
results.push({
|
|
1970
|
+
sessionId: detail.session.id,
|
|
1971
|
+
cwd: detail.session.cwd,
|
|
1972
|
+
timestamp: classified.entry.timestamp,
|
|
1973
|
+
kind: classified.kind,
|
|
1974
|
+
content,
|
|
1975
|
+
snippet: buildSnippet(content, normalizedQuery)
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
return {
|
|
1980
|
+
query,
|
|
1981
|
+
totalMatches: results.length,
|
|
1982
|
+
sessionsSearched: sessions.length,
|
|
1983
|
+
results
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
getConfig() {
|
|
1987
|
+
return this.configManager.getConfig();
|
|
1988
|
+
}
|
|
1989
|
+
updateConfig(key, value) {
|
|
1990
|
+
const current = this.configManager.getConfig();
|
|
1991
|
+
if (!(key in current)) {
|
|
1992
|
+
return null;
|
|
1993
|
+
}
|
|
1994
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1995
|
+
return null;
|
|
1996
|
+
}
|
|
1997
|
+
return this.configManager.updateSection(
|
|
1998
|
+
key,
|
|
1999
|
+
value
|
|
2000
|
+
);
|
|
2001
|
+
}
|
|
2002
|
+
async findSessionById(sessionId) {
|
|
2003
|
+
const sessions = await this.getAllSessions();
|
|
2004
|
+
const match = sessions.find((session) => session.id === sessionId);
|
|
2005
|
+
return match ?? null;
|
|
2006
|
+
}
|
|
2007
|
+
async getAllSessions() {
|
|
2008
|
+
const cacheKey = DataCache.buildKey(SESSIONS_CACHE_PREFIX, "all");
|
|
2009
|
+
const cached = this.dataCache.get(cacheKey);
|
|
2010
|
+
if (cached) {
|
|
2011
|
+
return cached;
|
|
2012
|
+
}
|
|
2013
|
+
const sessions = await this.scanner.scanSessions();
|
|
2014
|
+
this.dataCache.set(cacheKey, sessions);
|
|
2015
|
+
return sessions;
|
|
2016
|
+
}
|
|
2017
|
+
async getSessionFileRevision(filePath) {
|
|
2018
|
+
try {
|
|
2019
|
+
const stats = await fs.promises.stat(filePath);
|
|
2020
|
+
return `${Math.floor(stats.mtimeMs)}:${stats.size}`;
|
|
2021
|
+
} catch {
|
|
2022
|
+
return UNKNOWN_REVISION;
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
exports.CodexServiceContext = CodexServiceContext;
|
|
2027
|
+
exports.createLogger = createLogger;
|