@link-assistant/hive-mind 1.50.7 โ 1.50.9
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/CHANGELOG.md +17 -0
- package/README.md +6 -0
- package/package.json +1 -1
- package/src/agent.prompts.lib.mjs +25 -37
- package/src/architecture-care.prompts.lib.mjs +11 -11
- package/src/claude.prompts.lib.mjs +31 -46
- package/src/codex.lib.mjs +481 -100
- package/src/codex.options.lib.mjs +52 -0
- package/src/codex.prompts.lib.mjs +84 -39
- package/src/experiments-examples.prompts.lib.mjs +7 -7
- package/src/github-merge-repo-actions.lib.mjs +66 -2
- package/src/github-merge.lib.mjs +3 -3
- package/src/hive.bootstrap.lib.mjs +32 -0
- package/src/hive.config.lib.mjs +3 -3
- package/src/hive.mjs +13 -20
- package/src/interactive-mode.lib.mjs +200 -265
- package/src/interactive-mode.shared.lib.mjs +133 -0
- package/src/limits.lib.mjs +339 -2
- package/src/models/index.mjs +21 -12
- package/src/opencode.prompts.lib.mjs +26 -38
- package/src/queue-config.lib.mjs +6 -0
- package/src/solve.auto-continue.lib.mjs +1 -0
- package/src/solve.auto-merge.lib.mjs +8 -4
- package/src/solve.bootstrap.lib.mjs +39 -0
- package/src/solve.config.lib.mjs +18 -13
- package/src/solve.mjs +35 -40
- package/src/solve.progress-monitoring.lib.mjs +10 -2
- package/src/solve.restart-shared.lib.mjs +13 -1
- package/src/solve.results.lib.mjs +43 -5
- package/src/solve.validation.lib.mjs +1 -1
- package/src/telegram-bot.mjs +4 -2
- package/src/telegram-solve-queue.helpers.lib.mjs +151 -0
- package/src/telegram-solve-queue.lib.mjs +82 -181
- package/src/version-info.lib.mjs +8 -5
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
236
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
265
|
-
if (argv.
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
606
|
+
if (argv.verbose) {
|
|
607
|
+
await log(output);
|
|
608
|
+
}
|
|
299
609
|
lastMessage = output;
|
|
300
610
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
if (!line
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
};
|