@agent-link/agent 0.1.263 → 0.1.265

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/codex.js ADDED
@@ -0,0 +1,1086 @@
1
+ /**
2
+ * Codex CLI process driver for AgentLink.
3
+ *
4
+ * MVP integration:
5
+ * - Each turn spawns `codex exec --json` (or `codex exec resume --json`)
6
+ * - Prompt is written to stdin to avoid command-line length limits
7
+ * - Codex thread_id is used as the AgentLink session id
8
+ * - JSONL stdout is translated into backend_event messages
9
+ * - Session history is read from ~/.codex/sessions/YYYY/MM/DD/*.jsonl
10
+ */
11
+ import { spawn, execFileSync } from 'child_process';
12
+ import { createInterface } from 'readline';
13
+ import { readdirSync, readFileSync, writeFileSync, existsSync, rmSync, mkdirSync, unlinkSync } from 'fs';
14
+ import { dirname, join, resolve } from 'path';
15
+ import { homedir, platform } from 'os';
16
+ import { processUserMessage } from './history-message-transform.js';
17
+ import { codexCapabilities } from './backends/types.js';
18
+ import { CONFIG_DIR } from './config.js';
19
+ import { getCleanEnv } from './sdk.js';
20
+ import { getProvider, resolveProviderCommand } from './providers.js';
21
+ import { FileCache } from './file-cache.js';
22
+ const conversations = new Map();
23
+ const sendFns = new Set();
24
+ const MODEL_PATTERN = /^[a-zA-Z0-9._-]+$/;
25
+ const TURN_HEARTBEAT_INTERVAL_MS = 15_000;
26
+ const CODEX_SESSION_LIST_LIMIT = 150;
27
+ const CODEX_SESSION_DIR = join(homedir(), '.codex', 'sessions');
28
+ const CODEX_TITLES_FILE = join(CONFIG_DIR, 'codex-session-titles.json');
29
+ function generateTurnId() {
30
+ return `turn_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
31
+ }
32
+ function sendFn(msg) {
33
+ for (const fn of sendFns) {
34
+ try {
35
+ fn(msg);
36
+ }
37
+ catch { /* ignore */ }
38
+ }
39
+ }
40
+ export function addCodexSendFn(fn) {
41
+ sendFns.add(fn);
42
+ return () => { sendFns.delete(fn); };
43
+ }
44
+ function sessionRef(sessionId) {
45
+ return {
46
+ backendType: 'codex',
47
+ backendSessionId: sessionId,
48
+ backendThreadId: sessionId,
49
+ providerSessionId: sessionId || null,
50
+ };
51
+ }
52
+ function startCodexTurn(state) {
53
+ state.turnActive = true;
54
+ state.turnId = generateTurnId();
55
+ state.stateVersion++;
56
+ state.startedToolIds.clear();
57
+ sendFn({
58
+ type: 'turn_started',
59
+ conversationId: state.conversationId,
60
+ claudeSessionId: state.codexSessionId,
61
+ turnId: state.turnId,
62
+ stateVersion: state.stateVersion,
63
+ });
64
+ if (!state.heartbeatTimer) {
65
+ state.heartbeatTimer = setInterval(() => {
66
+ if (!state.turnActive) {
67
+ if (state.heartbeatTimer)
68
+ clearInterval(state.heartbeatTimer);
69
+ state.heartbeatTimer = null;
70
+ return;
71
+ }
72
+ sendFn({
73
+ type: 'turn_heartbeat',
74
+ conversationId: state.conversationId,
75
+ claudeSessionId: state.codexSessionId,
76
+ turnId: state.turnId,
77
+ stateVersion: state.stateVersion,
78
+ phase: 'running',
79
+ lastActivityAt: Date.now(),
80
+ });
81
+ }, TURN_HEARTBEAT_INTERVAL_MS);
82
+ }
83
+ }
84
+ function finishCodexTurn(state) {
85
+ state.turnActive = false;
86
+ state.stateVersion++;
87
+ state.startedToolIds.clear();
88
+ if (state.heartbeatTimer) {
89
+ clearInterval(state.heartbeatTimer);
90
+ state.heartbeatTimer = null;
91
+ }
92
+ }
93
+ function emitCodexCancelled(conversationId, state) {
94
+ sendFn({
95
+ type: 'backend_event',
96
+ conversationId,
97
+ event: { type: 'turn_cancelled', session: sessionRef(state?.codexSessionId || '') },
98
+ });
99
+ sendFn({
100
+ type: 'execution_cancelled',
101
+ conversationId,
102
+ claudeSessionId: state?.codexSessionId || null,
103
+ turnId: state?.turnId || null,
104
+ stateVersion: state?.stateVersion || 0,
105
+ });
106
+ }
107
+ function buildCodexUsage(state, rawUsage) {
108
+ return {
109
+ provider: 'codex',
110
+ model: state.model || undefined,
111
+ inputTokens: numberOrUndefined(rawUsage?.input_tokens),
112
+ outputTokens: numberOrUndefined(rawUsage?.output_tokens),
113
+ cacheReadInputTokens: numberOrUndefined(rawUsage?.cached_input_tokens),
114
+ };
115
+ }
116
+ function numberOrUndefined(value) {
117
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
118
+ }
119
+ function sendCodexSetupError(state, message) {
120
+ sendFn({
121
+ type: 'backend_event',
122
+ conversationId: state.conversationId,
123
+ event: { type: 'error', message },
124
+ });
125
+ sendFn({
126
+ type: 'backend_event',
127
+ conversationId: state.conversationId,
128
+ event: { type: 'turn_completed', session: sessionRef(state.codexSessionId || '') },
129
+ });
130
+ sendFn({
131
+ type: 'turn_completed',
132
+ conversationId: state.conversationId,
133
+ workDir: state.workDir,
134
+ claudeSessionId: state.codexSessionId,
135
+ turnId: state.turnId,
136
+ stateVersion: state.stateVersion,
137
+ });
138
+ }
139
+ let resolvedCodexPath = null;
140
+ function resolveCmdWrapperDirectExecution(cmdPath) {
141
+ if (platform() !== 'win32')
142
+ return null;
143
+ if (!cmdPath.toLowerCase().endsWith('.cmd'))
144
+ return null;
145
+ try {
146
+ const content = readFileSync(cmdPath, 'utf-8');
147
+ const jsMatch = content.match(/%dp0%\\(.+?\.js)"/i) ||
148
+ content.match(/%dp0%\\(.+?\.js)/i);
149
+ if (jsMatch) {
150
+ const scriptPath = join(dirname(cmdPath), jsMatch[1].replace(/\\/g, '/'));
151
+ if (existsSync(scriptPath)) {
152
+ return { command: process.execPath, prefixArgs: [scriptPath] };
153
+ }
154
+ }
155
+ const exeMatch = content.match(/%dp0%\\(.+?\.exe)"/i) ||
156
+ content.match(/%dp0%\\(.+?\.exe)/i);
157
+ if (exeMatch) {
158
+ const exePath = join(dirname(cmdPath), exeMatch[1].replace(/\\/g, '/'));
159
+ if (existsSync(exePath)) {
160
+ return { command: exePath, prefixArgs: [] };
161
+ }
162
+ }
163
+ }
164
+ catch {
165
+ return null;
166
+ }
167
+ return null;
168
+ }
169
+ export function _resetResolvedCodexPathForTests() {
170
+ resolvedCodexPath = null;
171
+ }
172
+ function resolveCodexPath() {
173
+ if (resolvedCodexPath)
174
+ return resolvedCodexPath;
175
+ const isWin = platform() === 'win32';
176
+ try {
177
+ const bin = isWin ? 'where' : 'which';
178
+ const output = execFileSync(bin, ['codex'], {
179
+ stdio: ['pipe', 'pipe', 'pipe'],
180
+ timeout: 5000,
181
+ env: getCleanEnv(),
182
+ windowsHide: true,
183
+ }).toString().trim();
184
+ const lines = output.split('\n').map(l => l.trim()).filter(Boolean);
185
+ resolvedCodexPath = (isWin && lines.length > 1)
186
+ ? (lines.find(l => /\.(cmd|exe)$/i.test(l)) || lines[0])
187
+ : (lines[0] || null);
188
+ }
189
+ catch {
190
+ if (isWin) {
191
+ try {
192
+ const psOutput = execFileSync('powershell.exe', [
193
+ '-NoProfile', '-Command',
194
+ '(Get-Command codex -ErrorAction SilentlyContinue).Source',
195
+ ], {
196
+ stdio: ['pipe', 'pipe', 'pipe'],
197
+ timeout: 8000,
198
+ env: getCleanEnv(),
199
+ windowsHide: true,
200
+ }).toString().trim();
201
+ if (psOutput && existsSync(psOutput))
202
+ resolvedCodexPath = psOutput;
203
+ }
204
+ catch { /* ignore */ }
205
+ }
206
+ }
207
+ if (!resolvedCodexPath)
208
+ resolvedCodexPath = 'codex';
209
+ console.log(`[Codex] Resolved command: ${resolvedCodexPath}`);
210
+ return resolvedCodexPath;
211
+ }
212
+ export function getCodexConversation(conversationId) {
213
+ return conversations.get(conversationId);
214
+ }
215
+ export function getCodexConversations() {
216
+ return conversations;
217
+ }
218
+ export function rebindCodexConversation(codexSessionId, newConvId) {
219
+ for (const [oldConvId, state] of conversations) {
220
+ if ((state.codexSessionId === codexSessionId || state.lastCodexSessionId === codexSessionId)
221
+ && oldConvId !== newConvId) {
222
+ conversations.delete(oldConvId);
223
+ state.conversationId = newConvId;
224
+ conversations.set(newConvId, state);
225
+ return true;
226
+ }
227
+ }
228
+ return false;
229
+ }
230
+ export function createCodexPlaceholder(conversationId, opts) {
231
+ if (conversations.has(conversationId))
232
+ return;
233
+ conversations.set(conversationId, {
234
+ conversationId,
235
+ child: null,
236
+ abortController: null,
237
+ codexSessionId: null,
238
+ lastCodexSessionId: null,
239
+ workDir: process.cwd(),
240
+ turnActive: false,
241
+ providerId: opts?.providerId || 'codex',
242
+ model: null,
243
+ turnId: null,
244
+ stateVersion: 0,
245
+ heartbeatTimer: null,
246
+ sessionNotified: false,
247
+ startedToolIds: new Set(),
248
+ });
249
+ }
250
+ export function restartCodexConversation(conversationId, opts) {
251
+ const state = conversations.get(conversationId);
252
+ const wasTurnActive = state?.turnActive || false;
253
+ const codexSessionId = state?.codexSessionId || null;
254
+ if (state)
255
+ cleanupCodexConversation(conversationId);
256
+ conversations.set(conversationId, {
257
+ conversationId,
258
+ child: null,
259
+ abortController: null,
260
+ codexSessionId: null,
261
+ lastCodexSessionId: codexSessionId,
262
+ workDir: state?.workDir || process.cwd(),
263
+ turnActive: false,
264
+ providerId: opts?.providerId || state?.providerId || 'codex',
265
+ model: opts?.model || state?.model || null,
266
+ turnId: null,
267
+ stateVersion: (state?.stateVersion || 0) + 1,
268
+ heartbeatTimer: null,
269
+ sessionNotified: false,
270
+ startedToolIds: new Set(),
271
+ });
272
+ return { wasTurnActive, codexSessionId };
273
+ }
274
+ function normalizeCodexRunOptions(input) {
275
+ const options = {};
276
+ if (!input)
277
+ return { options };
278
+ if (input.model != null && input.model !== '') {
279
+ if (!MODEL_PATTERN.test(input.model)) {
280
+ return { options, error: `Invalid Codex model name: ${input.model}` };
281
+ }
282
+ options.model = input.model;
283
+ }
284
+ return { options };
285
+ }
286
+ export function handleCodexChat(conversationId, text, workDir, options, files) {
287
+ let state = conversations.get(conversationId);
288
+ if (!state) {
289
+ state = {
290
+ conversationId,
291
+ child: null,
292
+ abortController: null,
293
+ codexSessionId: null,
294
+ lastCodexSessionId: null,
295
+ workDir,
296
+ turnActive: false,
297
+ providerId: 'codex',
298
+ model: null,
299
+ turnId: null,
300
+ stateVersion: 0,
301
+ heartbeatTimer: null,
302
+ sessionNotified: false,
303
+ startedToolIds: new Set(),
304
+ };
305
+ conversations.set(conversationId, state);
306
+ }
307
+ state.workDir = workDir;
308
+ if (options?.providerId)
309
+ state.providerId = options.providerId;
310
+ const resumeId = options?.resumeSessionId
311
+ || state.codexSessionId
312
+ || state.lastCodexSessionId;
313
+ if (resumeId) {
314
+ state.codexSessionId = resumeId;
315
+ state.lastCodexSessionId = resumeId;
316
+ }
317
+ const normalized = normalizeCodexRunOptions(options?.codexOptions);
318
+ if (normalized.error) {
319
+ sendCodexSetupError(state, normalized.error);
320
+ return;
321
+ }
322
+ if (normalized.options.model)
323
+ state.model = normalized.options.model;
324
+ if (files?.length) {
325
+ sendCodexSetupError(state, 'Codex attachments are not supported yet.');
326
+ return;
327
+ }
328
+ const args = resumeId
329
+ ? ['exec', 'resume', '--json', '--dangerously-bypass-approvals-and-sandbox']
330
+ : ['exec', '--json', '--dangerously-bypass-approvals-and-sandbox', '-C', workDir];
331
+ const model = state.model;
332
+ if (model)
333
+ args.push('--model', model);
334
+ if (resumeId)
335
+ args.push(resumeId);
336
+ args.push('-');
337
+ const controller = new AbortController();
338
+ state.abortController = controller;
339
+ startCodexTurn(state);
340
+ sendFn({
341
+ type: 'backend_event',
342
+ conversationId,
343
+ event: {
344
+ type: 'turn_started',
345
+ session: sessionRef(state.codexSessionId || 'pending'),
346
+ },
347
+ });
348
+ const provider = getProvider(state.providerId);
349
+ const { command, prefixArgs, spawnOpts } = state.providerId === 'codex'
350
+ ? { command: resolveCodexPath(), prefixArgs: [], spawnOpts: {} }
351
+ : resolveProviderCommand(provider);
352
+ const isWin = platform() === 'win32';
353
+ const direct = resolveCmdWrapperDirectExecution(command);
354
+ let spawnCmd;
355
+ let spawnArgs;
356
+ if (direct) {
357
+ spawnCmd = direct.command;
358
+ spawnArgs = [...direct.prefixArgs, ...prefixArgs, ...args];
359
+ }
360
+ else if (isWin && command.toLowerCase().endsWith('.cmd')) {
361
+ spawnCmd = process.env.COMSPEC || 'cmd.exe';
362
+ spawnArgs = ['/c', command, ...prefixArgs, ...args];
363
+ }
364
+ else {
365
+ spawnCmd = command;
366
+ spawnArgs = [...prefixArgs, ...args];
367
+ }
368
+ const child = spawn(spawnCmd, spawnArgs, {
369
+ cwd: provider.cwd || workDir,
370
+ stdio: ['pipe', 'pipe', 'pipe'],
371
+ env: { ...getCleanEnv(), ...(provider.env || {}) },
372
+ signal: controller.signal,
373
+ ...spawnOpts,
374
+ windowsHide: true,
375
+ });
376
+ state.child = child;
377
+ child.stdin?.end(text);
378
+ const rl = createInterface({ input: child.stdout });
379
+ let stderrBuf = '';
380
+ child.stderr?.on('data', (chunk) => {
381
+ stderrBuf += chunk.toString();
382
+ });
383
+ rl.on('line', (line) => {
384
+ if (!line.trim())
385
+ return;
386
+ try {
387
+ const event = JSON.parse(line);
388
+ handleCodexEvent(state, event);
389
+ }
390
+ catch {
391
+ stderrBuf += `${line}\n`;
392
+ }
393
+ });
394
+ child.on('close', (code) => {
395
+ if (state.child !== child)
396
+ return;
397
+ state.child = null;
398
+ state.abortController = null;
399
+ if (state.turnActive) {
400
+ finishCodexTurn(state);
401
+ if (code !== 0 && code !== null) {
402
+ const errMsg = stderrBuf.trim() || `Codex process exited with code ${code}`;
403
+ sendFn({
404
+ type: 'backend_event',
405
+ conversationId,
406
+ event: { type: 'error', message: errMsg },
407
+ });
408
+ }
409
+ sendFn({
410
+ type: 'backend_event',
411
+ conversationId,
412
+ event: { type: 'turn_completed', session: sessionRef(state.codexSessionId || '') },
413
+ });
414
+ sendFn({
415
+ type: 'turn_completed',
416
+ conversationId,
417
+ workDir: state.workDir,
418
+ claudeSessionId: state.codexSessionId,
419
+ turnId: state.turnId,
420
+ stateVersion: state.stateVersion,
421
+ });
422
+ }
423
+ });
424
+ child.on('error', (err) => {
425
+ if (state.child !== child)
426
+ return;
427
+ state.child = null;
428
+ finishCodexTurn(state);
429
+ if (!resumeId)
430
+ state.codexSessionId = null;
431
+ sendFn({
432
+ type: 'backend_event',
433
+ conversationId,
434
+ event: { type: 'error', message: `Failed to start codex: ${err.message}` },
435
+ });
436
+ sendFn({
437
+ type: 'backend_event',
438
+ conversationId,
439
+ event: { type: 'turn_completed', session: sessionRef(state.codexSessionId || '') },
440
+ });
441
+ sendFn({
442
+ type: 'turn_completed',
443
+ conversationId,
444
+ workDir: state.workDir,
445
+ claudeSessionId: state.codexSessionId,
446
+ turnId: state.turnId,
447
+ stateVersion: state.stateVersion,
448
+ });
449
+ });
450
+ }
451
+ function handleCodexEvent(state, event) {
452
+ const convId = state.conversationId;
453
+ switch (event.type) {
454
+ case 'thread.started': {
455
+ const threadId = event.thread_id;
456
+ if (threadId) {
457
+ state.codexSessionId = threadId;
458
+ state.lastCodexSessionId = threadId;
459
+ if (!state.sessionNotified) {
460
+ state.sessionNotified = true;
461
+ sendFn({
462
+ type: 'backend_event',
463
+ conversationId: convId,
464
+ event: { type: 'session_started', session: sessionRef(threadId) },
465
+ });
466
+ }
467
+ }
468
+ break;
469
+ }
470
+ case 'item.started': {
471
+ const item = event.item;
472
+ if (item && isToolItem(item))
473
+ emitToolStarted(state, item);
474
+ break;
475
+ }
476
+ case 'item.completed': {
477
+ const item = event.item;
478
+ if (!item)
479
+ break;
480
+ const itemType = String(item.type || '');
481
+ if (itemType === 'agent_message') {
482
+ const text = textFromUnknown(item.text);
483
+ if (text) {
484
+ sendFn({
485
+ type: 'backend_event',
486
+ conversationId: convId,
487
+ event: { type: 'text_delta', session: sessionRef(state.codexSessionId || 'pending'), text },
488
+ });
489
+ }
490
+ }
491
+ else if (itemType === 'reasoning') {
492
+ const text = textFromUnknown(item.text);
493
+ if (text) {
494
+ sendFn({
495
+ type: 'backend_event',
496
+ conversationId: convId,
497
+ event: { type: 'reasoning_delta', session: sessionRef(state.codexSessionId || 'pending'), text },
498
+ });
499
+ }
500
+ }
501
+ else if (isToolItem(item)) {
502
+ emitToolCompleted(state, item);
503
+ }
504
+ break;
505
+ }
506
+ case 'turn.completed': {
507
+ finishCodexTurn(state);
508
+ const usage = buildCodexUsage(state, event.usage);
509
+ sendFn({
510
+ type: 'backend_event',
511
+ conversationId: convId,
512
+ event: {
513
+ type: 'turn_completed',
514
+ session: sessionRef(state.codexSessionId || ''),
515
+ usage,
516
+ },
517
+ });
518
+ sendFn({
519
+ type: 'turn_completed',
520
+ conversationId: convId,
521
+ workDir: state.workDir,
522
+ claudeSessionId: state.codexSessionId,
523
+ turnId: state.turnId,
524
+ stateVersion: state.stateVersion,
525
+ usage,
526
+ });
527
+ break;
528
+ }
529
+ case 'turn.failed':
530
+ case 'error': {
531
+ const message = textFromUnknown(event.message)
532
+ || textFromUnknown(event.error?.message)
533
+ || 'Codex turn failed';
534
+ sendFn({
535
+ type: 'backend_event',
536
+ conversationId: convId,
537
+ event: { type: 'error', message },
538
+ });
539
+ break;
540
+ }
541
+ }
542
+ }
543
+ function isToolItem(item) {
544
+ const type = String(item.type || '');
545
+ return !!type && !['agent_message', 'reasoning'].includes(type);
546
+ }
547
+ function toolId(item) {
548
+ return textFromUnknown(item.id) || `codex-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
549
+ }
550
+ function toolName(item) {
551
+ return textFromUnknown(item.type) || 'codex_item';
552
+ }
553
+ function emitToolStarted(state, item) {
554
+ const id = toolId(item);
555
+ if (state.startedToolIds.has(id))
556
+ return;
557
+ state.startedToolIds.add(id);
558
+ sendFn({
559
+ type: 'backend_event',
560
+ conversationId: state.conversationId,
561
+ event: {
562
+ type: 'tool_started',
563
+ session: sessionRef(state.codexSessionId || 'pending'),
564
+ tool: {
565
+ id,
566
+ name: toolName(item),
567
+ itemKind: toolName(item),
568
+ input: codexToolInput(item),
569
+ status: 'in_progress',
570
+ },
571
+ },
572
+ });
573
+ }
574
+ function emitToolCompleted(state, item) {
575
+ const id = toolId(item);
576
+ if (!state.startedToolIds.has(id))
577
+ emitToolStarted(state, item);
578
+ const status = textFromUnknown(item.status);
579
+ const exitCode = numberOrUndefined(item.exit_code);
580
+ const isError = status === 'failed' || (exitCode != null && exitCode !== 0);
581
+ sendFn({
582
+ type: 'backend_event',
583
+ conversationId: state.conversationId,
584
+ event: {
585
+ type: 'tool_completed',
586
+ session: sessionRef(state.codexSessionId || 'pending'),
587
+ tool: {
588
+ id,
589
+ name: toolName(item),
590
+ output: codexToolOutput(item),
591
+ isError,
592
+ status: 'completed',
593
+ },
594
+ },
595
+ });
596
+ }
597
+ function codexToolInput(item) {
598
+ if (item.command)
599
+ return { command: item.command };
600
+ return Object.fromEntries(Object.entries(item).filter(([key]) => !['id', 'type', 'text', 'aggregated_output', 'output', 'status', 'exit_code'].includes(key)));
601
+ }
602
+ function codexToolOutput(item) {
603
+ return textFromUnknown(item.aggregated_output)
604
+ || textFromUnknown(item.output)
605
+ || textFromUnknown(item.text)
606
+ || JSON.stringify(item, null, 2);
607
+ }
608
+ export function cancelCodexExecution(conversationId) {
609
+ const state = conversations.get(conversationId);
610
+ if (!state) {
611
+ emitCodexCancelled(conversationId);
612
+ return;
613
+ }
614
+ if (!state.child) {
615
+ if (state.turnActive)
616
+ finishCodexTurn(state);
617
+ emitCodexCancelled(conversationId, state);
618
+ return;
619
+ }
620
+ finishCodexTurn(state);
621
+ emitCodexCancelled(conversationId, state);
622
+ if (platform() === 'win32') {
623
+ try {
624
+ spawn('cmd', ['/c', 'taskkill', '/pid', String(state.child.pid), '/f', '/t'], {
625
+ stdio: 'ignore',
626
+ windowsHide: true,
627
+ });
628
+ }
629
+ catch { /* ignore */ }
630
+ }
631
+ else {
632
+ state.child.kill('SIGTERM');
633
+ }
634
+ state.child = null;
635
+ state.abortController = null;
636
+ }
637
+ function cleanupCodexConversation(conversationId) {
638
+ const state = conversations.get(conversationId);
639
+ if (!state)
640
+ return;
641
+ if (state.child)
642
+ cancelCodexExecution(conversationId);
643
+ if (state.heartbeatTimer) {
644
+ clearInterval(state.heartbeatTimer);
645
+ state.heartbeatTimer = null;
646
+ }
647
+ if (state.codexSessionId)
648
+ state.lastCodexSessionId = state.codexSessionId;
649
+ conversations.delete(conversationId);
650
+ }
651
+ export function cleanupAllCodexConversations() {
652
+ for (const convId of conversations.keys())
653
+ cleanupCodexConversation(convId);
654
+ }
655
+ export function evictByCodexSessionId(codexSessionId) {
656
+ for (const [convId, conv] of conversations) {
657
+ if (conv.codexSessionId === codexSessionId || conv.lastCodexSessionId === codexSessionId) {
658
+ if (conv.turnActive)
659
+ return true;
660
+ cleanupCodexConversation(convId);
661
+ }
662
+ }
663
+ return false;
664
+ }
665
+ function normalizeCursorPath(value) {
666
+ const normalized = value.replace(/\\/g, '/').replace(/\/+$/, '');
667
+ return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
668
+ }
669
+ function normalizeSessionLimit(limit) {
670
+ if (limit == null)
671
+ return CODEX_SESSION_LIST_LIMIT;
672
+ if (limit === Number.POSITIVE_INFINITY)
673
+ return Number.POSITIVE_INFINITY;
674
+ if (!Number.isFinite(limit) || limit <= 0)
675
+ return CODEX_SESSION_LIST_LIMIT;
676
+ return Math.min(Math.floor(limit), 500);
677
+ }
678
+ function compareCursorSortKeys(a, b) {
679
+ const lastModifiedDiff = b.lastModified - a.lastModified;
680
+ if (lastModifiedDiff !== 0)
681
+ return lastModifiedDiff;
682
+ const sessionDiff = a.sessionId.localeCompare(b.sessionId);
683
+ if (sessionDiff !== 0)
684
+ return sessionDiff;
685
+ return a.tieBreaker.localeCompare(b.tieBreaker);
686
+ }
687
+ function encodeCodexCursor(scope, sortKey, workDir) {
688
+ return Buffer.from(JSON.stringify({
689
+ v: 1,
690
+ provider: 'codex',
691
+ scope,
692
+ ...(scope === 'workdir' && workDir ? { workDir: normalizeCursorPath(workDir) } : {}),
693
+ lastModified: sortKey.lastModified,
694
+ sessionId: sortKey.sessionId,
695
+ tieBreaker: sortKey.tieBreaker,
696
+ }), 'utf-8').toString('base64url');
697
+ }
698
+ function decodeCodexCursor(cursor, scope, workDir) {
699
+ let parsed;
700
+ try {
701
+ parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf-8'));
702
+ }
703
+ catch {
704
+ throw new Error('Invalid codex session list cursor');
705
+ }
706
+ if (!parsed || typeof parsed !== 'object')
707
+ throw new Error('Invalid codex session list cursor');
708
+ const value = parsed;
709
+ if (value.v !== 1
710
+ || value.provider !== 'codex'
711
+ || value.scope !== scope
712
+ || typeof value.lastModified !== 'number'
713
+ || !Number.isFinite(value.lastModified)
714
+ || typeof value.sessionId !== 'string'
715
+ || typeof value.tieBreaker !== 'string') {
716
+ throw new Error('Invalid codex session list cursor');
717
+ }
718
+ if (scope === 'workdir' && value.workDir !== normalizeCursorPath(workDir || '')) {
719
+ throw new Error('Codex session list cursor does not match requested workDir');
720
+ }
721
+ return { lastModified: value.lastModified, sessionId: value.sessionId, tieBreaker: value.tieBreaker };
722
+ }
723
+ function listCodexSessionFiles(root = CODEX_SESSION_DIR) {
724
+ if (!existsSync(root))
725
+ return [];
726
+ const result = [];
727
+ const stack = [root];
728
+ while (stack.length > 0) {
729
+ const dir = stack.pop();
730
+ let entries;
731
+ try {
732
+ entries = readdirSync(dir, { withFileTypes: true });
733
+ }
734
+ catch {
735
+ continue;
736
+ }
737
+ for (const entry of entries) {
738
+ const full = join(dir, entry.name);
739
+ if (entry.isDirectory())
740
+ stack.push(full);
741
+ else if (entry.isFile() && entry.name.endsWith('.jsonl'))
742
+ result.push(full);
743
+ }
744
+ }
745
+ return result;
746
+ }
747
+ function loadTitleMap() {
748
+ try {
749
+ const raw = readFileSync(CODEX_TITLES_FILE, 'utf-8');
750
+ const parsed = JSON.parse(raw);
751
+ return parsed && typeof parsed === 'object' ? parsed : {};
752
+ }
753
+ catch {
754
+ return {};
755
+ }
756
+ }
757
+ function writeTitleMap(map) {
758
+ if (!existsSync(CONFIG_DIR))
759
+ mkdirSync(CONFIG_DIR, { recursive: true });
760
+ writeFileSync(CODEX_TITLES_FILE, JSON.stringify(map, null, 2) + '\n', 'utf-8');
761
+ }
762
+ function setCustomTitle(sessionId, title) {
763
+ const map = loadTitleMap();
764
+ map[sessionId] = title;
765
+ writeTitleMap(map);
766
+ codexSessionCache.invalidateAll();
767
+ }
768
+ function removeCustomTitle(sessionId) {
769
+ const map = loadTitleMap();
770
+ if (Object.prototype.hasOwnProperty.call(map, sessionId)) {
771
+ delete map[sessionId];
772
+ writeTitleMap(map);
773
+ }
774
+ }
775
+ function textFromUnknown(value) {
776
+ return typeof value === 'string' ? value : '';
777
+ }
778
+ function contentText(content) {
779
+ if (typeof content === 'string')
780
+ return content;
781
+ if (!Array.isArray(content))
782
+ return '';
783
+ return content
784
+ .map((item) => {
785
+ if (!item || typeof item !== 'object')
786
+ return '';
787
+ const record = item;
788
+ return textFromUnknown(record.text)
789
+ || textFromUnknown(record.content)
790
+ || textFromUnknown(record.delta?.text);
791
+ })
792
+ .filter(Boolean)
793
+ .join('');
794
+ }
795
+ function cleanCodexUserText(text) {
796
+ return text
797
+ .replace(/<environment_context>[\s\S]*?<\/environment_context>/g, '')
798
+ .trim();
799
+ }
800
+ function sessionIdFromPath(filePath) {
801
+ const name = filePath.replace(/\\/g, '/').split('/').pop() || '';
802
+ const match = name.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i);
803
+ return match?.[0] || name.replace(/\.jsonl$/, '');
804
+ }
805
+ function parseTimestamp(value) {
806
+ if (typeof value !== 'string')
807
+ return 0;
808
+ const parsed = Date.parse(value);
809
+ return Number.isNaN(parsed) ? 0 : parsed;
810
+ }
811
+ function parseCodexSessionJsonl(filePath, stats) {
812
+ let sessionId = sessionIdFromPath(filePath);
813
+ let firstPrompt = '';
814
+ let lastAssistant = '';
815
+ let projectPath = '';
816
+ let lastModified = 0;
817
+ const content = readFileSync(filePath, 'utf-8');
818
+ for (const line of content.split('\n')) {
819
+ if (!line.trim())
820
+ continue;
821
+ try {
822
+ const row = JSON.parse(line);
823
+ const payload = row.payload;
824
+ if (!payload)
825
+ continue;
826
+ const ts = parseTimestamp(row.timestamp);
827
+ if (ts > lastModified)
828
+ lastModified = ts;
829
+ if (row.type === 'session_meta') {
830
+ if (typeof payload.id === 'string')
831
+ sessionId = payload.id;
832
+ if (typeof payload.cwd === 'string')
833
+ projectPath = payload.cwd;
834
+ continue;
835
+ }
836
+ if (typeof payload.cwd === 'string' && !projectPath)
837
+ projectPath = payload.cwd;
838
+ const payloadType = String(payload.type || '');
839
+ if (row.type === 'event_msg' && payloadType === 'user_message') {
840
+ const msg = cleanCodexUserText(textFromUnknown(payload.message));
841
+ if (msg && !firstPrompt)
842
+ firstPrompt = msg.substring(0, 100);
843
+ }
844
+ else if (row.type === 'event_msg' && payloadType === 'agent_message') {
845
+ const msg = textFromUnknown(payload.message).trim();
846
+ if (msg)
847
+ lastAssistant = msg.substring(0, 120);
848
+ }
849
+ else if (row.type === 'event_msg' && payloadType === 'task_complete') {
850
+ const msg = textFromUnknown(payload.last_agent_message).trim();
851
+ if (msg)
852
+ lastAssistant = msg.substring(0, 120);
853
+ }
854
+ else if (row.type === 'response_item' && payload.type === 'message') {
855
+ const text = cleanCodexUserText(contentText(payload.content));
856
+ if (payload.role === 'user' && text && !firstPrompt)
857
+ firstPrompt = text.substring(0, 100);
858
+ if (payload.role === 'assistant' && text)
859
+ lastAssistant = text.substring(0, 120);
860
+ }
861
+ }
862
+ catch { /* skip malformed lines */ }
863
+ }
864
+ const titleMap = loadTitleMap();
865
+ const customTitle = titleMap[sessionId];
866
+ const title = customTitle || firstPrompt || lastAssistant || sessionId.slice(0, 8);
867
+ if (!projectPath)
868
+ projectPath = '';
869
+ return {
870
+ sessionId,
871
+ title,
872
+ ...(customTitle ? { customTitle } : {}),
873
+ preview: lastAssistant || firstPrompt || title,
874
+ firstPrompt: firstPrompt || title,
875
+ lastModified: lastModified || stats.mtimeMs,
876
+ projectPath,
877
+ projectFolder: projectPath.replace(/[\\/]+$/, '').split(/[\\/]/).pop() || projectPath,
878
+ sourcePath: filePath,
879
+ };
880
+ }
881
+ const codexSessionCache = new FileCache({
882
+ name: 'codex-session-jsonl',
883
+ compute: parseCodexSessionJsonl,
884
+ maxEntries: 10000,
885
+ ttlMs: 0,
886
+ debug: false,
887
+ });
888
+ function getCodexSessionEntries(workDir) {
889
+ const normalizedWorkDir = workDir ? normalizeCursorPath(workDir) : '';
890
+ const entries = [];
891
+ for (const filePath of listCodexSessionFiles()) {
892
+ const parsed = codexSessionCache.get(filePath);
893
+ if (!parsed)
894
+ continue;
895
+ if (normalizedWorkDir && normalizeCursorPath(parsed.projectPath) !== normalizedWorkDir)
896
+ continue;
897
+ entries.push({
898
+ session: parsed,
899
+ sortKey: {
900
+ lastModified: parsed.lastModified,
901
+ sessionId: parsed.sessionId,
902
+ tieBreaker: normalizeCursorPath(filePath),
903
+ },
904
+ });
905
+ }
906
+ entries.sort((a, b) => compareCursorSortKeys(a.sortKey, b.sortKey));
907
+ return entries;
908
+ }
909
+ export function listCodexSessions(workDir) {
910
+ return listCodexSessionsPage(workDir, { limit: Number.POSITIVE_INFINITY }).sessions;
911
+ }
912
+ export function listCodexSessionsPage(workDir, options = {}) {
913
+ const entries = getCodexSessionEntries(workDir);
914
+ const limit = normalizeSessionLimit(options.limit);
915
+ let startIndex = 0;
916
+ if (options.cursor) {
917
+ const cursor = decodeCodexCursor(options.cursor, 'workdir', workDir);
918
+ startIndex = entries.findIndex(entry => compareCursorSortKeys(entry.sortKey, cursor) > 0);
919
+ if (startIndex < 0)
920
+ return { sessions: [], hasMore: false };
921
+ }
922
+ const pageEntries = limit === Number.POSITIVE_INFINITY
923
+ ? entries.slice(startIndex)
924
+ : entries.slice(startIndex, startIndex + limit);
925
+ const nextIndex = startIndex + pageEntries.length;
926
+ const hasMore = nextIndex < entries.length;
927
+ return {
928
+ sessions: pageEntries.map(({ session }) => ({
929
+ sessionId: session.sessionId,
930
+ title: session.title,
931
+ customTitle: session.customTitle,
932
+ preview: session.preview,
933
+ lastModified: session.lastModified,
934
+ })),
935
+ hasMore,
936
+ ...(hasMore && pageEntries.length > 0 ? { nextCursor: encodeCodexCursor('workdir', pageEntries[pageEntries.length - 1].sortKey, workDir) } : {}),
937
+ };
938
+ }
939
+ export function listAllCodexSessions(limit = 500) {
940
+ return listAllCodexSessionsPage({ limit }).sessions;
941
+ }
942
+ export function listAllCodexSessionsPage(options = {}) {
943
+ const entries = getCodexSessionEntries();
944
+ const limit = normalizeSessionLimit(options.limit);
945
+ let startIndex = 0;
946
+ if (options.cursor) {
947
+ const cursor = decodeCodexCursor(options.cursor, 'global');
948
+ startIndex = entries.findIndex(entry => compareCursorSortKeys(entry.sortKey, cursor) > 0);
949
+ if (startIndex < 0)
950
+ return { sessions: [], hasMore: false, indexState: 'ready' };
951
+ }
952
+ const pageEntries = limit === Number.POSITIVE_INFINITY
953
+ ? entries.slice(startIndex)
954
+ : entries.slice(startIndex, startIndex + limit);
955
+ const nextIndex = startIndex + pageEntries.length;
956
+ const hasMore = nextIndex < entries.length;
957
+ return {
958
+ sessions: pageEntries.map(({ session }) => ({
959
+ sessionId: session.sessionId,
960
+ projectPath: session.projectPath,
961
+ projectFolder: session.projectFolder,
962
+ stableIdentity: normalizeCursorPath(session.sourcePath),
963
+ title: session.title,
964
+ firstPrompt: session.firstPrompt,
965
+ lastModified: session.lastModified,
966
+ })),
967
+ hasMore,
968
+ ...(hasMore && pageEntries.length > 0 ? { nextCursor: encodeCodexCursor('global', pageEntries[pageEntries.length - 1].sortKey) } : {}),
969
+ indexState: 'ready',
970
+ };
971
+ }
972
+ function findCodexSessionFile(sessionId) {
973
+ for (const filePath of listCodexSessionFiles()) {
974
+ const parsed = codexSessionCache.get(filePath);
975
+ if (parsed?.sessionId === sessionId)
976
+ return filePath;
977
+ }
978
+ return null;
979
+ }
980
+ function pushUniqueMessage(messages, seen, msg) {
981
+ const key = `${msg.role}\n${msg.timestamp || ''}\n${msg.content}\n${msg.toolName || ''}`;
982
+ if (seen.has(key))
983
+ return;
984
+ seen.add(key);
985
+ messages.push(msg);
986
+ }
987
+ export function readCodexSessionMessages(_workDir, sessionId) {
988
+ const filePath = findCodexSessionFile(sessionId);
989
+ if (!filePath)
990
+ return [];
991
+ const messages = [];
992
+ const seen = new Set();
993
+ try {
994
+ const content = readFileSync(filePath, 'utf-8');
995
+ for (const line of content.split('\n')) {
996
+ if (!line.trim())
997
+ continue;
998
+ try {
999
+ const row = JSON.parse(line);
1000
+ const payload = row.payload;
1001
+ if (!payload)
1002
+ continue;
1003
+ const ts = typeof row.timestamp === 'string' ? row.timestamp : undefined;
1004
+ const payloadType = String(payload.type || '');
1005
+ if (row.type === 'event_msg' && payloadType === 'user_message') {
1006
+ const text = cleanCodexUserText(textFromUnknown(payload.message));
1007
+ if (!text.trim())
1008
+ continue;
1009
+ const processed = processUserMessage(text, ts);
1010
+ for (const entry of processed.prefixEntries)
1011
+ pushUniqueMessage(messages, seen, entry);
1012
+ if (processed.userEntry)
1013
+ pushUniqueMessage(messages, seen, processed.userEntry);
1014
+ }
1015
+ else if (row.type === 'event_msg' && payloadType === 'agent_message') {
1016
+ const text = textFromUnknown(payload.message);
1017
+ if (text.trim())
1018
+ pushUniqueMessage(messages, seen, { role: 'assistant', content: text, timestamp: ts });
1019
+ }
1020
+ else if (row.type === 'response_item' && payload.type === 'message') {
1021
+ const text = payload.role === 'user'
1022
+ ? cleanCodexUserText(contentText(payload.content))
1023
+ : contentText(payload.content);
1024
+ if (!text.trim())
1025
+ continue;
1026
+ if (payload.role === 'user') {
1027
+ const processed = processUserMessage(text, ts);
1028
+ for (const entry of processed.prefixEntries)
1029
+ pushUniqueMessage(messages, seen, entry);
1030
+ if (processed.userEntry)
1031
+ pushUniqueMessage(messages, seen, processed.userEntry);
1032
+ }
1033
+ else if (payload.role === 'assistant') {
1034
+ pushUniqueMessage(messages, seen, { role: 'assistant', content: text, timestamp: ts });
1035
+ }
1036
+ }
1037
+ else if (row.type === 'response_item' && payload.type && payload.type !== 'message') {
1038
+ pushUniqueMessage(messages, seen, {
1039
+ role: 'tool',
1040
+ content: '',
1041
+ toolName: String(payload.type),
1042
+ toolInput: JSON.stringify(payload, null, 2),
1043
+ toolId: textFromUnknown(payload.id),
1044
+ timestamp: ts,
1045
+ });
1046
+ }
1047
+ }
1048
+ catch { /* skip malformed lines */ }
1049
+ }
1050
+ }
1051
+ catch { /* skip unreadable files */ }
1052
+ return messages;
1053
+ }
1054
+ export function renameCodexSession(_workDir, sessionId, newTitle) {
1055
+ if (!findCodexSessionFile(sessionId))
1056
+ return false;
1057
+ setCustomTitle(sessionId, newTitle);
1058
+ return true;
1059
+ }
1060
+ export function deleteCodexSession(_workDir, sessionId) {
1061
+ const filePath = findCodexSessionFile(sessionId);
1062
+ if (!filePath)
1063
+ return false;
1064
+ const resolved = resolve(filePath);
1065
+ if (!resolved.startsWith(resolve(CODEX_SESSION_DIR)))
1066
+ return false;
1067
+ try {
1068
+ unlinkSync(resolved);
1069
+ codexSessionCache.invalidate(resolved);
1070
+ removeCustomTitle(sessionId);
1071
+ return true;
1072
+ }
1073
+ catch {
1074
+ try {
1075
+ rmSync(resolved, { force: true });
1076
+ codexSessionCache.invalidate(resolved);
1077
+ removeCustomTitle(sessionId);
1078
+ return true;
1079
+ }
1080
+ catch {
1081
+ return false;
1082
+ }
1083
+ }
1084
+ }
1085
+ export { codexCapabilities };
1086
+ //# sourceMappingURL=codex.js.map