@link-assistant/hive-mind 1.50.8 โ†’ 1.50.10

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/src/codex.lib.mjs CHANGED
@@ -18,27 +18,271 @@ import { reportError } from './sentry.lib.mjs';
18
18
  import { timeouts } from './config.lib.mjs';
19
19
  import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
20
20
  import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
21
+ import { mapModelToId, resolveCodexReasoningEffort } from './codex.options.lib.mjs';
22
+ import { createInteractiveHandler } from './interactive-mode.lib.mjs';
23
+ import { initProgressMonitoring } from './solve.progress-monitoring.lib.mjs';
24
+
25
+ const CODEX_USAGE_FIELD_NAMES = ['input_tokens', 'cached_input_tokens', 'output_tokens'];
26
+ const getCodexExecEnv = (verbose = false) => (verbose ? { ...process.env, RUST_LOG: 'debug' } : { ...process.env });
27
+ const CODEX_MODEL_DIAGNOSTIC_PATHS = [
28
+ ['model', data => data?.model],
29
+ ['model_name', data => data?.model_name],
30
+ ['from_model', data => data?.from_model],
31
+ ['to_model', data => data?.to_model],
32
+ ['message.model', data => data?.message?.model],
33
+ ];
34
+
35
+ export const createCodexTokenUsage = requestedModelId => ({
36
+ inputTokens: 0,
37
+ outputTokens: 0,
38
+ reasoningTokens: 0,
39
+ cacheReadTokens: 0,
40
+ cacheWriteTokens: 0,
41
+ totalTokens: 0,
42
+ stepCount: 0,
43
+ requestedModelId: requestedModelId || null,
44
+ respondedModelId: requestedModelId || null,
45
+ });
46
+
47
+ const createEmptyCodexItemUsage = () => ({
48
+ inputTokens: 0,
49
+ cacheCreationTokens: 0,
50
+ cacheReadTokens: 0,
51
+ outputTokens: 0,
52
+ totalTokens: null,
53
+ });
54
+
55
+ const upsertById = (items, nextItem) => {
56
+ const existingIndex = items.findIndex(item => item.id === nextItem.id);
57
+ if (existingIndex >= 0) {
58
+ items[existingIndex] = { ...items[existingIndex], ...nextItem };
59
+ } else {
60
+ items.push(nextItem);
61
+ }
62
+ };
63
+
64
+ const upsertCodexSubAgentCall = (subAgentCalls, item, requestedModelId = null) => {
65
+ const nextCall = {
66
+ id: item.id || null,
67
+ description: item.prompt || `${item.tool || 'collab_tool_call'} via codex`,
68
+ model: requestedModelId || null,
69
+ tool: item.tool || null,
70
+ senderThreadId: item.sender_thread_id || null,
71
+ receiverThreadIds: Array.isArray(item.receiver_thread_ids) ? item.receiver_thread_ids : [],
72
+ agentsStates: item.agents_states || {},
73
+ status: item.status || null,
74
+ usage: subAgentCalls.find(call => call.id === item.id)?.usage || createEmptyCodexItemUsage(),
75
+ };
76
+
77
+ upsertById(subAgentCalls, nextCall);
78
+ };
79
+
80
+ const upsertCodexCommandExecution = (commandExecutions, item) => {
81
+ upsertById(commandExecutions, {
82
+ id: item.id || null,
83
+ command: item.command || null,
84
+ aggregatedOutput: item.aggregated_output || '',
85
+ exitCode: item.exit_code ?? null,
86
+ status: item.status || null,
87
+ });
88
+ };
89
+
90
+ const upsertCodexFileChange = (fileChanges, item) => {
91
+ upsertById(fileChanges, {
92
+ id: item.id || null,
93
+ status: item.status || null,
94
+ changes: Array.isArray(item.changes)
95
+ ? item.changes.map(change => ({
96
+ path: change?.path || null,
97
+ kind: change?.kind || null,
98
+ }))
99
+ : [],
100
+ });
101
+ };
102
+
103
+ const upsertCodexMcpToolCall = (mcpToolCalls, item) => {
104
+ upsertById(mcpToolCalls, {
105
+ id: item.id || null,
106
+ server: item.server || null,
107
+ tool: item.tool || null,
108
+ arguments: item.arguments ?? null,
109
+ result: item.result ?? null,
110
+ error: item.error ?? null,
111
+ status: item.status || null,
112
+ });
113
+ };
114
+
115
+ const upsertCodexWebSearch = (webSearches, item) => {
116
+ upsertById(webSearches, {
117
+ id: item.id || null,
118
+ searchId: item.id || null,
119
+ query: item.query || null,
120
+ action: item.action || null,
121
+ });
122
+ };
21
123
 
22
- // Model mapping to translate aliases to full model IDs for Codex
23
- export const mapModelToId = model => {
24
- const modelMap = {
25
- gpt5: 'gpt-5',
26
- 'gpt5-codex': 'gpt-5-codex',
27
- o3: 'o3',
28
- 'o3-mini': 'o3-mini',
29
- gpt4: 'gpt-4',
30
- gpt4o: 'gpt-4o',
31
- claude: 'claude-3-5-sonnet',
32
- sonnet: 'claude-3-5-sonnet',
33
- opus: 'claude-3-opus',
124
+ const upsertCodexTodoList = (todoLists, item) => {
125
+ upsertById(todoLists, {
126
+ id: item.id || null,
127
+ items: Array.isArray(item.items)
128
+ ? item.items.map(todo => ({
129
+ text: todo?.text || '',
130
+ completed: !!todo?.completed,
131
+ }))
132
+ : [],
133
+ });
134
+ };
135
+
136
+ const upsertCodexItemError = (itemErrors, item) => {
137
+ upsertById(itemErrors, {
138
+ id: item.id || null,
139
+ message: item.message || '',
140
+ });
141
+ };
142
+
143
+ export const parseCodexExecJsonOutput = (output, state = {}, requestedModelId = null) => {
144
+ const nextState = {
145
+ sessionId: state.sessionId || null,
146
+ authError: state.authError || false,
147
+ resultSummary: state.resultSummary || '',
148
+ tokenUsage: state.tokenUsage || createCodexTokenUsage(requestedModelId),
149
+ eventCounts: state.eventCounts || {},
150
+ itemTypeCounts: state.itemTypeCounts || {},
151
+ subAgentCalls: state.subAgentCalls || [],
152
+ reasoningSummaries: state.reasoningSummaries || [],
153
+ commandExecutions: state.commandExecutions || [],
154
+ fileChanges: state.fileChanges || [],
155
+ mcpToolCalls: state.mcpToolCalls || [],
156
+ webSearches: state.webSearches || [],
157
+ todoLists: state.todoLists || [],
158
+ itemErrors: state.itemErrors || [],
159
+ turnFailures: state.turnFailures || [],
160
+ streamErrors: state.streamErrors || [],
161
+ observedUsageFieldSets: state.observedUsageFieldSets || [],
162
+ observedModelDiagnosticPaths: state.observedModelDiagnosticPaths || [],
34
163
  };
35
164
 
36
- // Return mapped model ID if it's an alias, otherwise return as-is
37
- return modelMap[model] || model;
165
+ const observedModelPaths = new Set(nextState.observedModelDiagnosticPaths);
166
+
167
+ for (const rawLine of output.split('\n')) {
168
+ const line = rawLine.trim();
169
+ if (!line) continue;
170
+
171
+ let data;
172
+ try {
173
+ data = sanitizeObjectStrings(JSON.parse(line));
174
+ } catch {
175
+ continue;
176
+ }
177
+
178
+ const eventType = typeof data.type === 'string' ? data.type : 'unknown';
179
+ nextState.eventCounts[eventType] = (nextState.eventCounts[eventType] || 0) + 1;
180
+
181
+ if (eventType === 'thread.started' && typeof data.thread_id === 'string' && !nextState.sessionId) {
182
+ nextState.sessionId = data.thread_id;
183
+ } else if (!nextState.sessionId && typeof data.session_id === 'string') {
184
+ nextState.sessionId = data.session_id;
185
+ }
186
+
187
+ for (const [pathName, getter] of CODEX_MODEL_DIAGNOSTIC_PATHS) {
188
+ if (typeof getter(data) === 'string') observedModelPaths.add(pathName);
189
+ }
190
+
191
+ if (eventType === 'error' && typeof data.message === 'string' && (data.message.includes('401 Unauthorized') || data.message.includes('401') || data.message.includes('Unauthorized'))) {
192
+ nextState.authError = true;
193
+ }
194
+
195
+ if (eventType === 'error' && typeof data.message === 'string') {
196
+ nextState.streamErrors.push({ message: data.message });
197
+ }
198
+
199
+ if (eventType === 'turn.failed' && typeof data.error?.message === 'string' && (data.error.message.includes('401 Unauthorized') || data.error.message.includes('401') || data.error.message.includes('Unauthorized'))) {
200
+ nextState.authError = true;
201
+ }
202
+
203
+ if (eventType === 'turn.failed' && typeof data.error?.message === 'string') {
204
+ nextState.turnFailures.push({ message: data.error.message });
205
+ }
206
+
207
+ if (eventType === 'turn.completed' && data.usage && typeof data.usage === 'object') {
208
+ const inputTokens = Number.isFinite(data.usage.input_tokens) ? data.usage.input_tokens : 0;
209
+ const cachedInputTokens = Number.isFinite(data.usage.cached_input_tokens) ? data.usage.cached_input_tokens : 0;
210
+ const outputTokens = Number.isFinite(data.usage.output_tokens) ? data.usage.output_tokens : 0;
211
+ const nonCachedInputTokens = Math.max(0, inputTokens - cachedInputTokens);
212
+ nextState.tokenUsage.inputTokens += nonCachedInputTokens;
213
+ nextState.tokenUsage.cacheReadTokens += cachedInputTokens;
214
+ nextState.tokenUsage.outputTokens += outputTokens;
215
+ nextState.tokenUsage.totalTokens = nextState.tokenUsage.inputTokens + nextState.tokenUsage.cacheReadTokens + nextState.tokenUsage.outputTokens + nextState.tokenUsage.cacheWriteTokens;
216
+ nextState.tokenUsage.stepCount += 1;
217
+
218
+ const usageFieldSet = CODEX_USAGE_FIELD_NAMES.filter(fieldName => Object.hasOwn(data.usage, fieldName));
219
+ if (usageFieldSet.length > 0) nextState.observedUsageFieldSets.push(usageFieldSet);
220
+ }
221
+
222
+ const item = data.item;
223
+ const itemType = typeof item?.type === 'string' ? item.type : null;
224
+ if (itemType) nextState.itemTypeCounts[itemType] = (nextState.itemTypeCounts[itemType] || 0) + 1;
225
+
226
+ if ((eventType === 'item.completed' || eventType === 'item.updated') && itemType === 'agent_message' && typeof item.text === 'string' && item.text.trim()) {
227
+ nextState.resultSummary = item.text;
228
+ }
229
+
230
+ if ((eventType === 'item.completed' || eventType === 'item.updated') && itemType === 'reasoning' && typeof item.text === 'string' && item.text.trim()) {
231
+ nextState.reasoningSummaries.push(item.text);
232
+ }
233
+
234
+ if ((eventType === 'item.completed' || eventType === 'item.updated') && itemType === 'collab_tool_call' && item && typeof item === 'object') {
235
+ upsertCodexSubAgentCall(nextState.subAgentCalls, item, requestedModelId);
236
+ }
237
+
238
+ if ((eventType === 'item.started' || eventType === 'item.updated' || eventType === 'item.completed') && itemType === 'command_execution' && item && typeof item === 'object') {
239
+ upsertCodexCommandExecution(nextState.commandExecutions, item);
240
+ }
241
+
242
+ if ((eventType === 'item.started' || eventType === 'item.updated' || eventType === 'item.completed') && itemType === 'file_change' && item && typeof item === 'object') {
243
+ upsertCodexFileChange(nextState.fileChanges, item);
244
+ }
245
+
246
+ if ((eventType === 'item.started' || eventType === 'item.updated' || eventType === 'item.completed') && itemType === 'mcp_tool_call' && item && typeof item === 'object') {
247
+ upsertCodexMcpToolCall(nextState.mcpToolCalls, item);
248
+ }
249
+
250
+ if ((eventType === 'item.started' || eventType === 'item.updated' || eventType === 'item.completed') && itemType === 'web_search' && item && typeof item === 'object') {
251
+ upsertCodexWebSearch(nextState.webSearches, item);
252
+ }
253
+
254
+ if ((eventType === 'item.started' || eventType === 'item.updated' || eventType === 'item.completed') && itemType === 'todo_list' && item && typeof item === 'object') {
255
+ upsertCodexTodoList(nextState.todoLists, item);
256
+ }
257
+
258
+ if ((eventType === 'item.started' || eventType === 'item.updated' || eventType === 'item.completed') && itemType === 'error' && item && typeof item === 'object') {
259
+ upsertCodexItemError(nextState.itemErrors, item);
260
+ }
261
+ }
262
+
263
+ nextState.observedModelDiagnosticPaths = [...observedModelPaths];
264
+ return nextState;
265
+ };
266
+
267
+ export const buildCodexResultModelUsage = (modelId, tokenUsage, pricingInfo = null) => {
268
+ if (!modelId || !tokenUsage) return null;
269
+
270
+ return {
271
+ [modelId]: {
272
+ inputTokens: tokenUsage.inputTokens || 0,
273
+ cacheCreationTokens: tokenUsage.cacheWriteTokens || 0,
274
+ cacheReadTokens: tokenUsage.cacheReadTokens || 0,
275
+ outputTokens: tokenUsage.outputTokens || 0,
276
+ modelName: pricingInfo?.modelName || modelId,
277
+ modelInfo: pricingInfo?.modelInfo || null,
278
+ peakContextUsage: 0,
279
+ costUSD: pricingInfo?.totalCostUSD ?? null,
280
+ },
281
+ };
38
282
  };
39
283
 
40
284
  // Function to validate Codex CLI connection
41
- export const validateCodexConnection = async (model = 'gpt-5') => {
285
+ export const validateCodexConnection = async (model = 'gpt-5.4', verbose = false) => {
42
286
  // Map model alias to full ID
43
287
  const mappedModel = mapModelToId(model);
44
288
 
@@ -71,7 +315,7 @@ export const validateCodexConnection = async (model = 'gpt-5') => {
71
315
 
72
316
  // Test basic Codex functionality with a simple "echo hi" command
73
317
  // Using exec mode with JSON output for validation
74
- const testResult = await $`printf "echo hi" | timeout ${Math.floor(timeouts.codexCli / 1000)} codex exec --model ${mappedModel} --json --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox`;
318
+ const testResult = await $({ env: getCodexExecEnv(verbose) })`printf "echo hi" | timeout ${Math.floor(timeouts.codexCli / 1000)} codex exec --model ${mappedModel} --json --skip-git-repo-check -c model_reasoning_effort="none" --dangerously-bypass-approvals-and-sandbox`;
75
319
 
76
320
  if (testResult.code !== 0) {
77
321
  const stderr = testResult.stderr?.toString() || '';
@@ -114,12 +358,41 @@ export const handleCodexRuntimeSwitch = async () => {
114
358
  await log('โ„น๏ธ Codex runtime handling not required for this operation');
115
359
  };
116
360
 
361
+ /** Check if Playwright MCP is available and connected to Codex @returns {Promise<boolean>} */
362
+ export const checkPlaywrightMcpAvailability = async () => {
363
+ try {
364
+ const result = await $`timeout 5 codex mcp list 2>&1`.catch(() => null);
365
+ if (!result || result.code !== 0) return false;
366
+ const output = `${result.stdout?.toString() || ''}${result.stderr?.toString() || ''}`;
367
+ return output.toLowerCase().includes('playwright');
368
+ } catch {
369
+ return false;
370
+ }
371
+ };
372
+
117
373
  // Main function to execute Codex with prompts and settings
118
374
  export const executeCodex = async params => {
119
375
  const { issueUrl, issueNumber, prNumber, prUrl, branchName, tempDir, workspaceTmpDir, isContinueMode, mergeStateStatus, forkedRepo, feedbackLines, forkActionsUrl, owner, repo, argv, log, formatAligned, getResourceSnapshot, codexPath = 'codex', $ } = params;
120
376
 
377
+ if (argv.promptSubagentsViaAgentCommander) {
378
+ try {
379
+ await $`which start-agent`;
380
+ argv.agentCommanderInstalled = true;
381
+ } catch {
382
+ argv.agentCommanderInstalled = false;
383
+ await log('โš ๏ธ agent-commander not installed; prompt guidance will be skipped (npm i -g @link-assistant/agent-commander)');
384
+ }
385
+ }
386
+
121
387
  // Import prompt building functions from codex.prompts.lib.mjs
122
388
  const { buildUserPrompt, buildSystemPrompt } = await import('./codex.prompts.lib.mjs');
389
+ const { checkModelVisionCapability } = await import('./claude.lib.mjs');
390
+ const mappedModel = mapModelToId(argv.model);
391
+ const modelSupportsVision = await checkModelVisionCapability(mappedModel);
392
+
393
+ if (argv.verbose) {
394
+ await log(`๐Ÿ‘๏ธ Model vision capability: ${modelSupportsVision ? 'supported' : 'not supported'}`, { verbose: true });
395
+ }
123
396
 
124
397
  // Build the user prompt
125
398
  const prompt = buildUserPrompt({
@@ -152,6 +425,7 @@ export const executeCodex = async params => {
152
425
  isContinueMode,
153
426
  forkedRepo,
154
427
  argv,
428
+ modelSupportsVision,
155
429
  });
156
430
 
157
431
  // Log prompt details in verbose mode
@@ -189,11 +463,16 @@ export const executeCodex = async params => {
189
463
  feedbackLines,
190
464
  codexPath,
191
465
  $,
466
+ owner,
467
+ repo,
468
+ prNumber,
192
469
  });
193
470
  };
194
471
 
195
472
  export const executeCodexCommand = async params => {
196
- const { tempDir, branchName, prompt, systemPrompt, argv, log, formatAligned, getResourceSnapshot, forkedRepo, feedbackLines, codexPath, $ } = params;
473
+ const { tempDir, branchName, prompt, systemPrompt, argv, log, formatAligned, getResourceSnapshot, forkedRepo, feedbackLines, codexPath, $, owner, repo, prNumber } = params;
474
+
475
+ const shellQuote = value => `"${String(value).replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
197
476
 
198
477
  // Retry configuration
199
478
  const maxRetries = 3;
@@ -226,23 +505,11 @@ export const executeCodexCommand = async params => {
226
505
  await log(` Memory: ${resourcesBefore.memory.split('\n')[1]}`, { verbose: true });
227
506
  await log(` Load: ${resourcesBefore.load}`, { verbose: true });
228
507
 
229
- // Build Codex command
230
508
  let execCommand;
231
-
232
- // Map model alias to full ID
233
509
  const mappedModel = mapModelToId(argv.model);
234
-
235
- // Build codex command arguments
236
- // Codex uses exec mode for non-interactive execution
237
- // --json provides structured output
238
- // --full-auto enables automatic execution with workspace-write sandbox
239
- let codexArgs = `exec --model ${mappedModel} --json --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox`;
240
-
241
- if (argv.resume) {
242
- // Codex supports resuming sessions
243
- await log(`๐Ÿ”„ Resuming from session: ${argv.resume}`);
244
- codexArgs = `exec resume ${argv.resume} --json --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox`;
245
- }
510
+ const { reasoningEffort, source: reasoningEffortSource } = resolveCodexReasoningEffort(argv);
511
+ const isResumeMode = !!argv.resume;
512
+ const codexEnv = getCodexExecEnv(argv.verbose);
246
513
 
247
514
  // For Codex, we combine system and user prompts into a single message
248
515
  // Codex doesn't have separate system prompt support in CLI mode
@@ -251,33 +518,54 @@ export const executeCodexCommand = async params => {
251
518
  // Write the combined prompt to a file for piping
252
519
  // Use OS temporary directory instead of repository workspace to avoid polluting the repo
253
520
  const promptFile = path.join(os.tmpdir(), `codex_prompt_${Date.now()}_${process.pid}.txt`);
521
+ const lastMessageFile = path.join(os.tmpdir(), `codex_last_message_${Date.now()}_${process.pid}.txt`);
254
522
  await fs.writeFile(promptFile, combinedPrompt);
255
523
 
256
- // Build the full command - pipe the prompt file to codex
257
- const fullCommand = `(cd "${tempDir}" && cat "${promptFile}" | ${codexPath} ${codexArgs})`;
524
+ await log(` Resolved model ID: ${mappedModel}`, { verbose: true });
525
+ await log(` Execution mode: ${isResumeMode ? 'resume' : 'new exec'}`, { verbose: true });
526
+ await log(` Prompt file: ${promptFile}`, { verbose: true });
527
+ await log(` Last message file: ${lastMessageFile}`, { verbose: true });
528
+ if (argv.verbose && codexEnv.RUST_LOG) {
529
+ await log(` Codex debug env: RUST_LOG=${codexEnv.RUST_LOG}`, { verbose: true });
530
+ }
531
+
532
+ // Build codex command arguments once so the logged command matches the executed command.
533
+ let codexArgs = 'exec';
534
+ if (isResumeMode) {
535
+ await log(`๐Ÿ”„ Resuming from session: ${argv.resume}`);
536
+ codexArgs += ` resume ${shellQuote(argv.resume)}`;
537
+ } else {
538
+ codexArgs += ` --model ${shellQuote(mappedModel)}`;
539
+ }
540
+ codexArgs += ` --json --skip-git-repo-check -o ${shellQuote(lastMessageFile)} -c ${shellQuote(`model_reasoning_effort=${reasoningEffort}`)} -c ${shellQuote('model_reasoning_summary=auto')} --dangerously-bypass-approvals-and-sandbox`;
541
+
542
+ const fullCommand = `(cd ${shellQuote(tempDir)} && cat ${shellQuote(promptFile)} | ${codexPath} ${codexArgs})`;
258
543
 
259
544
  await log(`\n${formatAligned('๐Ÿ“', 'Raw command:', '')}`);
260
545
  await log(`${fullCommand}`);
261
546
  await log('');
262
547
 
263
548
  try {
264
- // Pipe the prompt file to codex via stdin
265
- if (argv.resume) {
266
- execCommand = $({
267
- cwd: tempDir,
268
- mirror: false,
269
- })`cat ${promptFile} | ${codexPath} exec resume ${argv.resume} --json --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox`;
270
- } else {
271
- execCommand = $({
272
- cwd: tempDir,
273
- mirror: false,
274
- })`cat ${promptFile} | ${codexPath} exec --model ${mappedModel} --json --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox`;
549
+ let interactiveHandler = null;
550
+ if (argv.interactiveMode && owner && repo && prNumber) {
551
+ await log('๐Ÿ”Œ Interactive mode: Creating handler for real-time PR comments', { verbose: true });
552
+ interactiveHandler = createInteractiveHandler({ owner, repo, prNumber, $, log, verbose: argv.verbose });
553
+ } else if (argv.interactiveMode) {
554
+ await log('โš ๏ธ Interactive mode: Disabled - missing PR info (owner/repo/prNumber)', { verbose: true });
275
555
  }
556
+ const progressMonitor = await initProgressMonitoring(argv, { owner, repo, prNumber, $, log });
557
+
558
+ execCommand = $({
559
+ cwd: tempDir,
560
+ mirror: false,
561
+ env: codexEnv,
562
+ })`sh -lc ${fullCommand}`;
276
563
 
277
564
  await log(`${formatAligned('๐Ÿ“‹', 'Command details:', '')}`);
278
565
  await log(formatAligned('๐Ÿ“‚', 'Working directory:', tempDir, 2));
279
566
  await log(formatAligned('๐ŸŒฟ', 'Branch:', branchName, 2));
280
567
  await log(formatAligned('๐Ÿค–', 'Model:', `Codex ${argv.model.toUpperCase()}`, 2));
568
+ await log(formatAligned('๐Ÿง ', 'Reasoning effort:', `${reasoningEffort} (${reasoningEffortSource})`, 2));
281
569
  if (argv.fork && forkedRepo) {
282
570
  await log(formatAligned('๐Ÿด', 'Fork:', forkedRepo, 2));
283
571
  }
@@ -291,76 +579,71 @@ export const executeCodexCommand = async params => {
291
579
  let lastMessage = '';
292
580
  let lastTextContent = ''; // Issue #1263: Track last text content for result summary
293
581
  let authError = false;
582
+ let codexJsonState = {
583
+ sessionId: null,
584
+ authError: false,
585
+ resultSummary: '',
586
+ tokenUsage: createCodexTokenUsage(mappedModel),
587
+ eventCounts: {},
588
+ itemTypeCounts: {},
589
+ subAgentCalls: [],
590
+ reasoningSummaries: [],
591
+ commandExecutions: [],
592
+ fileChanges: [],
593
+ mcpToolCalls: [],
594
+ webSearches: [],
595
+ todoLists: [],
596
+ itemErrors: [],
597
+ turnFailures: [],
598
+ streamErrors: [],
599
+ observedUsageFieldSets: [],
600
+ observedModelDiagnosticPaths: [],
601
+ };
294
602
 
295
603
  for await (const chunk of execCommand.stream()) {
296
604
  if (chunk.type === 'stdout') {
297
605
  const output = chunk.data.toString();
298
- await log(output);
606
+ if (argv.verbose) {
607
+ await log(output);
608
+ }
299
609
  lastMessage = output;
300
610
 
301
- // Try to parse JSON output to extract session info
302
- // Codex CLI uses thread_id instead of session_id
303
- try {
304
- const lines = output.split('\n');
305
- for (const line of lines) {
306
- if (!line.trim()) continue;
307
- const data = sanitizeObjectStrings(JSON.parse(line));
308
- // Check for both thread_id (codex) and session_id (legacy)
309
- if ((data.thread_id || data.session_id) && !sessionId) {
310
- sessionId = data.thread_id || data.session_id;
311
- await log(`๐Ÿ“Œ Session ID: ${sessionId}`);
611
+ codexJsonState = parseCodexExecJsonOutput(output, codexJsonState, mappedModel);
612
+
613
+ if (interactiveHandler || progressMonitor) {
614
+ for (const rawLine of output.split('\n')) {
615
+ const line = rawLine.trim();
616
+ if (!line) continue;
617
+ try {
618
+ const data = sanitizeObjectStrings(JSON.parse(line));
619
+ if (interactiveHandler) await interactiveHandler.processEvent(data);
620
+ if (progressMonitor) await progressMonitor.processStreamEvent(data);
621
+ } catch {
622
+ // Ignore non-JSON lines
312
623
  }
624
+ }
625
+ }
313
626
 
314
- // Check for authentication errors (401 Unauthorized)
315
- // These should never be retried as they indicate missing/invalid credentials
316
- if (data.type === 'error' && data.message && (data.message.includes('401 Unauthorized') || data.message.includes('401') || data.message.includes('Unauthorized'))) {
317
- authError = true;
318
- await log('\nโŒ Authentication error detected: 401 Unauthorized', { level: 'error' });
319
- await log(' This error cannot be resolved by retrying.', { level: 'error' });
320
- await log(' ๐Ÿ’ก Please run: codex login', { level: 'error' });
321
- }
627
+ if (codexJsonState.sessionId && codexJsonState.sessionId !== sessionId) {
628
+ sessionId = codexJsonState.sessionId;
629
+ await log(`๐Ÿ“Œ Session ID: ${sessionId}`);
630
+ }
322
631
 
323
- // Also check turn.failed events for auth errors
324
- if (data.type === 'turn.failed' && data.error && data.error.message && (data.error.message.includes('401 Unauthorized') || data.error.message.includes('401') || data.error.message.includes('Unauthorized'))) {
325
- authError = true;
326
- await log('\nโŒ Authentication error detected in turn.failed event', { level: 'error' });
327
- await log(' This error cannot be resolved by retrying.', { level: 'error' });
328
- await log(' ๐Ÿ’ก Please run: codex login', { level: 'error' });
329
- }
632
+ if (codexJsonState.resultSummary) {
633
+ lastTextContent = codexJsonState.resultSummary;
634
+ }
330
635
 
331
- // Issue #1263: Track text content for result summary
332
- // Codex outputs text via 'text', 'assistant', 'message', or 'result' type events
333
- if (data.type === 'text' && data.text) {
334
- lastTextContent = data.text;
335
- } else if (data.type === 'assistant' && data.message?.content) {
336
- const content = Array.isArray(data.message.content) ? data.message.content : [data.message.content];
337
- for (const item of content) {
338
- if (item.type === 'text' && item.text) {
339
- lastTextContent = item.text;
340
- }
341
- }
342
- } else if (data.type === 'message' && data.content) {
343
- if (typeof data.content === 'string') {
344
- lastTextContent = data.content;
345
- } else if (Array.isArray(data.content)) {
346
- for (const item of data.content) {
347
- if (item.type === 'text' && item.text) {
348
- lastTextContent = item.text;
349
- }
350
- }
351
- }
352
- } else if (data.type === 'result' && data.result) {
353
- lastTextContent = data.result;
354
- }
355
- }
356
- } catch {
357
- // Not JSON, continue
636
+ if (codexJsonState.authError && !authError) {
637
+ authError = true;
638
+ await log('\nโŒ Authentication error detected in Codex JSON stream', { level: 'error' });
639
+ await log(' This error cannot be resolved by retrying.', { level: 'error' });
640
+ await log(' ๐Ÿ’ก Please run: codex login', { level: 'error' });
358
641
  }
359
642
  }
360
643
 
361
644
  if (chunk.type === 'stderr') {
362
645
  const errorOutput = chunk.data.toString();
363
- if (errorOutput) {
646
+ if (errorOutput && argv.verbose) {
364
647
  await log(errorOutput, { stream: 'stderr' });
365
648
  }
366
649
  } else if (chunk.type === 'exit') {
@@ -368,6 +651,86 @@ export const executeCodexCommand = async params => {
368
651
  }
369
652
  }
370
653
 
654
+ if (interactiveHandler) {
655
+ await interactiveHandler.flush();
656
+ }
657
+
658
+ try {
659
+ const lastMessageFromFile = (await fs.readFile(lastMessageFile, 'utf8')).trim();
660
+ if (lastMessageFromFile) {
661
+ await log(`๐Ÿ“ Final Codex message captured in ${lastMessageFile}`, { verbose: true });
662
+ await log(lastMessageFromFile, { verbose: true });
663
+ lastTextContent = lastTextContent || lastMessageFromFile;
664
+ } else {
665
+ await log(`โš ๏ธ Final Codex message file was empty: ${lastMessageFile}`, { level: 'warning', verbose: true });
666
+ }
667
+ } catch (readError) {
668
+ await log(`โš ๏ธ Could not read Codex final message file: ${readError.message}`, { level: 'warning', verbose: true });
669
+ }
670
+
671
+ if (Object.keys(codexJsonState.eventCounts).length > 0) {
672
+ const eventSummary = Object.entries(codexJsonState.eventCounts)
673
+ .map(([eventType, count]) => `${eventType}=${count}`)
674
+ .join(', ');
675
+ await log(`๐Ÿ“Š Codex JSON events: ${eventSummary}`, { verbose: true });
676
+ }
677
+ if (Object.keys(codexJsonState.itemTypeCounts).length > 0) {
678
+ const itemSummary = Object.entries(codexJsonState.itemTypeCounts)
679
+ .map(([itemType, count]) => `${itemType}=${count}`)
680
+ .join(', ');
681
+ await log(`๐Ÿ“ฆ Codex item types: ${itemSummary}`, { verbose: true });
682
+ }
683
+ if (codexJsonState.tokenUsage.stepCount > 0) {
684
+ await log(`๐Ÿ“ˆ Codex usage from turn.completed: ${codexJsonState.tokenUsage.inputTokens.toLocaleString()} input, ${codexJsonState.tokenUsage.cacheReadTokens.toLocaleString()} cache read, ${codexJsonState.tokenUsage.outputTokens.toLocaleString()} output across ${codexJsonState.tokenUsage.stepCount} turn(s)`, { verbose: true });
685
+ } else {
686
+ await log('๐Ÿ“ˆ No Codex usage found in turn.completed events', { level: 'warning', verbose: true });
687
+ }
688
+ if (codexJsonState.subAgentCalls.length > 0) {
689
+ await log(`๐Ÿค Codex collab/sub-agent calls observed: ${codexJsonState.subAgentCalls.length}`, { verbose: true });
690
+ }
691
+ if (codexJsonState.reasoningSummaries.length > 0) {
692
+ await log(`๐Ÿง  Codex reasoning summaries observed: ${codexJsonState.reasoningSummaries.length}`, { verbose: true });
693
+ }
694
+ if (codexJsonState.commandExecutions.length > 0) {
695
+ await log(`๐Ÿ’ป Codex command executions observed: ${codexJsonState.commandExecutions.length}`, { verbose: true });
696
+ }
697
+ if (codexJsonState.fileChanges.length > 0) {
698
+ await log(`๐Ÿ“ Codex file change items observed: ${codexJsonState.fileChanges.length}`, { verbose: true });
699
+ }
700
+ if (codexJsonState.mcpToolCalls.length > 0) {
701
+ await log(`๐Ÿ”Œ Codex MCP tool calls observed: ${codexJsonState.mcpToolCalls.length}`, { verbose: true });
702
+ }
703
+ if (codexJsonState.webSearches.length > 0) {
704
+ await log(`๐ŸŒ Codex web searches observed: ${codexJsonState.webSearches.length}`, { verbose: true });
705
+ }
706
+ if (codexJsonState.todoLists.length > 0) {
707
+ const latestTodoCount = codexJsonState.todoLists.at(-1)?.items?.length || 0;
708
+ await log(`๐Ÿ“‹ Codex todo list updates observed: ${codexJsonState.todoLists.length} (latest: ${latestTodoCount} items)`, { verbose: true });
709
+ }
710
+ if (codexJsonState.itemErrors.length > 0 || codexJsonState.turnFailures.length > 0 || codexJsonState.streamErrors.length > 0) {
711
+ await log(`โš ๏ธ Codex error events observed: item=${codexJsonState.itemErrors.length}, turn=${codexJsonState.turnFailures.length}, stream=${codexJsonState.streamErrors.length}`, { verbose: true });
712
+ }
713
+ if (codexJsonState.observedUsageFieldSets.length > 0) {
714
+ const lastUsageFieldSet = codexJsonState.observedUsageFieldSets.at(-1);
715
+ await log(`๐Ÿ“ Codex usage fields observed: ${lastUsageFieldSet.join(', ')}`, { verbose: true });
716
+ }
717
+ if (codexJsonState.observedModelDiagnosticPaths.length > 0) {
718
+ await log(`๐Ÿ”Ž Undocumented model-related JSON fields observed but ignored for accounting: ${codexJsonState.observedModelDiagnosticPaths.join(', ')}`, { verbose: true });
719
+ } else {
720
+ await log(`๐Ÿค– Codex exec JSON did not expose model IDs; using requested model for reporting: ${mappedModel}`, { verbose: true });
721
+ }
722
+
723
+ const firstActualModelId = mappedModel;
724
+ const pricingInfo = firstActualModelId
725
+ ? {
726
+ modelId: firstActualModelId,
727
+ modelName: firstActualModelId,
728
+ provider: 'OpenAI',
729
+ tokenUsage: codexJsonState.tokenUsage.stepCount > 0 ? codexJsonState.tokenUsage : null,
730
+ }
731
+ : null;
732
+ const resultModelUsage = pricingInfo?.tokenUsage ? buildCodexResultModelUsage(firstActualModelId, pricingInfo.tokenUsage, pricingInfo) : null;
733
+
371
734
  // Check for authentication errors first - these should never be retried
372
735
  if (authError) {
373
736
  const resourcesAfter = await getResourceSnapshot();
@@ -415,6 +778,10 @@ export const executeCodexCommand = async params => {
415
778
  sessionId,
416
779
  limitReached,
417
780
  limitResetTime,
781
+ pricingInfo,
782
+ resultModelUsage,
783
+ subAgentCalls: codexJsonState.subAgentCalls.length > 0 ? codexJsonState.subAgentCalls : null,
784
+ codexJsonDetails: codexJsonState,
418
785
  resultSummary: lastTextContent || null, // Issue #1263: Use last text content from JSON output stream
419
786
  };
420
787
  }
@@ -424,6 +791,8 @@ export const executeCodexCommand = async params => {
424
791
  // Issue #1263: Log if result summary was captured
425
792
  if (lastTextContent) {
426
793
  await log('๐Ÿ“ Captured result summary from Codex output', { verbose: true });
794
+ } else {
795
+ await log('โš ๏ธ No result summary captured from Codex output or last-message file', { level: 'warning', verbose: true });
427
796
  }
428
797
 
429
798
  return {
@@ -431,6 +800,10 @@ export const executeCodexCommand = async params => {
431
800
  sessionId,
432
801
  limitReached,
433
802
  limitResetTime,
803
+ pricingInfo,
804
+ resultModelUsage,
805
+ subAgentCalls: codexJsonState.subAgentCalls.length > 0 ? codexJsonState.subAgentCalls : null,
806
+ codexJsonDetails: codexJsonState,
434
807
  resultSummary: lastTextContent || null, // Issue #1263: Use last text content from JSON output stream
435
808
  };
436
809
  } catch (error) {
@@ -456,8 +829,14 @@ export const executeCodexCommand = async params => {
456
829
  sessionId: null,
457
830
  limitReached: false,
458
831
  limitResetTime: null,
832
+ pricingInfo: null,
459
833
  resultSummary: null, // Issue #1263: No result summary available on error
460
834
  };
835
+ } finally {
836
+ await log(`๐Ÿงน Removing temporary Codex prompt file: ${promptFile}`, { verbose: true });
837
+ await fs.rm(promptFile, { force: true }).catch(() => {});
838
+ await log(`๐Ÿงน Removing temporary Codex last-message file: ${lastMessageFile}`, { verbose: true });
839
+ await fs.rm(lastMessageFile, { force: true }).catch(() => {});
461
840
  }
462
841
  };
463
842
 
@@ -553,7 +932,9 @@ export const checkForUncommittedChanges = async (tempDir, owner, repo, branchNam
553
932
  export default {
554
933
  validateCodexConnection,
555
934
  handleCodexRuntimeSwitch,
935
+ checkPlaywrightMcpAvailability,
556
936
  executeCodex,
557
937
  executeCodexCommand,
938
+ resolveCodexReasoningEffort,
558
939
  checkForUncommittedChanges,
559
940
  };