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.
@@ -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;