@link-assistant/hive-mind 2.11.5 → 2.11.7
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 +21 -0
- package/package.json +1 -1
- package/src/agent.lib.mjs +13 -10
- package/src/bidirectional-interactive.lib.mjs +2 -1
- package/src/budget-stats-policy.lib.mjs +31 -0
- package/src/claude.lib.mjs +13 -14
- package/src/codex.lib.mjs +29 -70
- package/src/codex.run-diagnostics.lib.mjs +117 -0
- package/src/formal-ai-runtime.lib.mjs +439 -0
- package/src/formal-ai.lib.mjs +143 -41
- package/src/gemini.lib.mjs +6 -8
- package/src/git.lib.mjs +10 -1
- package/src/github-error-reporter.lib.mjs +2 -1
- package/src/github-rate-limit.lib.mjs +13 -0
- package/src/github-terminal-state.lib.mjs +7 -1
- package/src/github.lib.mjs +16 -8
- package/src/opencode.lib.mjs +11 -10
- package/src/post-finish-sanitization-sweep.lib.mjs +4 -3
- package/src/quiet-probe.lib.mjs +72 -0
- package/src/qwen.lib.mjs +8 -7
- package/src/solve.auto-continue.lib.mjs +2 -1
- package/src/solve.auto-merge.lib.mjs +14 -15
- package/src/solve.auto-pr.lib.mjs +11 -5
- package/src/solve.branch-errors.lib.mjs +2 -1
- package/src/solve.config.lib.mjs +2 -2
- package/src/solve.execution.lib.mjs +2 -1
- package/src/solve.feedback.lib.mjs +18 -13
- package/src/solve.fork-sync.lib.mjs +2 -1
- package/src/solve.mjs +0 -2
- package/src/solve.repo-setup.lib.mjs +40 -4
- package/src/solve.repository.lib.mjs +4 -3
- package/src/solve.results.lib.mjs +32 -26
- package/src/solve.validation.lib.mjs +7 -0
- package/src/solve.watch.lib.mjs +15 -15
- package/src/token-sanitization.lib.mjs +7 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.11.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 815a63c: Keep context/cost budget statistics out of the working session summary (issue #2132).
|
|
8
|
+
|
|
9
|
+
Cost estimation and token/context usage are now published only in the working
|
|
10
|
+
session log comment, and only when `--attach-logs` is enabled — previously every
|
|
11
|
+
session posted the identical blocks twice, and the summary could publish them even
|
|
12
|
+
with log attachment disabled. The per-session budget stats derivation used by the
|
|
13
|
+
top-level run, watch iterations and auto-restart-until-mergeable iterations is now
|
|
14
|
+
a single shared implementation.
|
|
15
|
+
|
|
16
|
+
## 2.11.6
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- 55b30c8: Make `--model formal-ai` work for every tool by talking to Formal AI directly instead of through the `formal-ai with` wrapper. Hive Mind now starts `formal-ai serve --agent-mode` and points each CLI at that endpoint through its own native configuration channel — `CODEX_HOME` for codex, a settings file for gemini, `OPENAI_MODEL` for qwen — so the wrapper can no longer rewrite the argv, drop the caller's prompt, or send codex traffic to `api.openai.com`. The operator's `HOME`, git, gh and ssh configuration is never shadowed.
|
|
21
|
+
|
|
22
|
+
Also removes the log noise and false verdicts the same runs exposed: read-only `gh`/`git` probes no longer mirror their raw payloads into the log that `--attach-logs` uploads (`gh auth status --show-token` was printing a live credential in clear text), codex no longer warns on every run, an expected 404 is no longer printed as an error, `gh auth setup-git` no longer fails on a bind-mounted `~/.gitconfig`, and "No working session summary available" is no longer reported for a session that produced one. The Formal AI wrapper version is recorded in the solve log, and the runtime logs its endpoint, protocol, config root and environment for the next iteration.
|
|
23
|
+
|
|
3
24
|
## 2.11.5
|
|
4
25
|
|
|
5
26
|
### Patch Changes
|
package/package.json
CHANGED
package/src/agent.lib.mjs
CHANGED
|
@@ -22,7 +22,7 @@ import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
|
|
|
22
22
|
import Decimal from 'decimal.js-light';
|
|
23
23
|
import semver from 'semver';
|
|
24
24
|
import { agentModels, defaultModels, freeToBaseModelMap, isFormalAiModel } from './models/index.mjs';
|
|
25
|
-
import { logPreparedToolCommand,
|
|
25
|
+
import { isPrepareOnly, logPreparedToolCommand, resolveFormalAiToolExecution } from './formal-ai.lib.mjs';
|
|
26
26
|
import { buildFormalAiPricingInfo } from './formal-ai-pricing.lib.mjs'; // Issue #2119
|
|
27
27
|
import { checkPlaywrightMcpPackageAvailability, getAgentPlaywrightMcpDisableEnv } from './playwright-mcp.lib.mjs';
|
|
28
28
|
import { createAgentTokenUsage, accumulateAgentStepFinishUsage, parseAgentTokenUsage } from './agent-token-usage.lib.mjs';
|
|
@@ -510,11 +510,9 @@ export const executeAgentCommand = async params => {
|
|
|
510
510
|
|
|
511
511
|
// Map model alias to full ID
|
|
512
512
|
const mappedModel = mapModelToId(argv.model);
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
toolPath: agentPath,
|
|
517
|
-
});
|
|
513
|
+
// Issue #2130: Formal AI runs the native CLI against a local Formal AI server (no argv wrapper).
|
|
514
|
+
const toolInvocation = await resolveFormalAiToolExecution({ tool: 'agent', model: argv.model, toolPath: agentPath, workdir: tempDir, log, verbose: argv.verbose, prepareOnly: isPrepareOnly(argv), env: agentEnv });
|
|
515
|
+
Object.assign(agentEnv, toolInvocation.env);
|
|
518
516
|
|
|
519
517
|
// Build agent command arguments
|
|
520
518
|
let agentArgs = `--model ${mappedModel}`;
|
|
@@ -571,7 +569,7 @@ export const executeAgentCommand = async params => {
|
|
|
571
569
|
mirror: false,
|
|
572
570
|
env: agentEnv,
|
|
573
571
|
});
|
|
574
|
-
execCommand =
|
|
572
|
+
execCommand = commandRunner`${toolInvocation.command} ${agentArgs}`;
|
|
575
573
|
const attached = await attachStreamingInput(bidirectionalHandler, execCommand, combinedPrompt, log, !!argv.verbose, { toolLabel: 'Agent' });
|
|
576
574
|
if (!attached) {
|
|
577
575
|
throw new Error('Agent live stream-json input requested, but stdin attachment failed');
|
|
@@ -583,7 +581,7 @@ export const executeAgentCommand = async params => {
|
|
|
583
581
|
mirror: false,
|
|
584
582
|
env: agentEnv,
|
|
585
583
|
});
|
|
586
|
-
execCommand =
|
|
584
|
+
execCommand = commandRunner`cat ${promptFile} | ${toolInvocation.command} ${agentArgs}`;
|
|
587
585
|
}
|
|
588
586
|
|
|
589
587
|
await log(`${formatAligned('📋', 'Command details:', '')}`);
|
|
@@ -675,8 +673,13 @@ export const executeAgentCommand = async params => {
|
|
|
675
673
|
}
|
|
676
674
|
// Issue #1263: Track text content for result summary
|
|
677
675
|
// Agent outputs text via 'text', 'assistant', or 'message' type events
|
|
678
|
-
|
|
679
|
-
|
|
676
|
+
// Issue #2130: Agent CLI 0.25.x nests the assistant text under `part`
|
|
677
|
+
// (`{"type":"text","part":{"type":"text","text":"…"}}`) and never sets a
|
|
678
|
+
// top-level `data.text`. Reading only `data.text` left `resultSummary`
|
|
679
|
+
// null for every successful run, which surfaced as the false negative
|
|
680
|
+
// "ℹ️ No working session summary available from AI tool output".
|
|
681
|
+
if (data.type === 'text' && (data.text || data.part?.text)) {
|
|
682
|
+
lastTextContent = data.text || data.part.text;
|
|
680
683
|
} else if (data.type === 'assistant' && data.message?.content) {
|
|
681
684
|
// Extract text from assistant message content
|
|
682
685
|
const content = Array.isArray(data.message.content) ? data.message.content : [data.message.content];
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { wrapDollarWithGhRetry as _wrapDollarWithGhRetry } from './github-rate-limit.lib.mjs'; // rate-limit marker (#1726): gh API calls flow through $ wrapped by caller
|
|
26
|
+
import { quietProbe } from './quiet-probe.lib.mjs'; // issue #2130: keep read-only probe payloads out of the attached log
|
|
26
27
|
import { getLiveInputCapability, getLiveInputCapabilityRows, getLiveInputMode, isLiveInputSupported, LIVE_INPUT_MODE_FALLBACK, LIVE_INPUT_MODE_STREAM } from './live-input-capabilities.lib.mjs';
|
|
27
28
|
// Configuration constants
|
|
28
29
|
const CONFIG = {
|
|
@@ -186,7 +187,7 @@ export const createBidirectionalHandler = options => {
|
|
|
186
187
|
const resolveOwnUserLogin = async () => {
|
|
187
188
|
if (ownUserResolved) return ownUserLogin;
|
|
188
189
|
try {
|
|
189
|
-
const result = await
|
|
190
|
+
const result = await quietProbe($)`gh api user --jq .login`;
|
|
190
191
|
ownUserLogin = (result.stdout?.toString() || '').trim() || null;
|
|
191
192
|
} catch (error) {
|
|
192
193
|
if (verbose) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Issue #2132: single source of truth for *where* context/cost budget statistics
|
|
5
|
+
* may be published.
|
|
6
|
+
*
|
|
7
|
+
* Rules encoded here:
|
|
8
|
+
* 1. Budget stats belong to the working session **log** comment only. The
|
|
9
|
+
* "Working session summary" comment reports what the AI did, never how many
|
|
10
|
+
* tokens or dollars it took (see `attachSolutionSummary`).
|
|
11
|
+
* 2. `--attach-logs` disabled ⇒ no log comment ⇒ no published budget stats.
|
|
12
|
+
* `--tokens-budget-stats` alone is not enough to publish them to GitHub; it
|
|
13
|
+
* only controls the local terminal rendering of the same facts.
|
|
14
|
+
*
|
|
15
|
+
* Keeping this in one module means the top-level run, the watch loop and the
|
|
16
|
+
* auto-restart-until-mergeable loop cannot drift apart again.
|
|
17
|
+
*
|
|
18
|
+
* @see https://github.com/link-assistant/hive-mind/issues/2132
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Whether `--attach-logs` is enabled (both camelCase and kebab-case forms). */
|
|
22
|
+
export const isAttachLogsEnabled = argv => !!(argv && (argv.attachLogs || argv['attach-logs']));
|
|
23
|
+
|
|
24
|
+
/** Whether `--tokens-budget-stats` is enabled (both camelCase and kebab-case forms). */
|
|
25
|
+
export const isTokensBudgetStatsEnabled = argv => !!(argv && (argv.tokensBudgetStats || argv['tokens-budget-stats']));
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether context/cost budget statistics may be published to GitHub for this run.
|
|
29
|
+
* Requires BOTH `--tokens-budget-stats` and `--attach-logs`.
|
|
30
|
+
*/
|
|
31
|
+
export const shouldPublishBudgetStats = argv => isTokensBudgetStatsEnabled(argv) && isAttachLogsEnabled(argv);
|
package/src/claude.lib.mjs
CHANGED
|
@@ -23,7 +23,7 @@ import { SESSION_FORCE_KILLED_MARKER, postTrackedComment } from './tool-comments
|
|
|
23
23
|
import { handleClaudeRuntimeSwitch } from './claude.runtime-switch.lib.mjs'; // see issue #1141
|
|
24
24
|
import { CLAUDE_MODELS as availableModels, mapClaudeSubAgentModelToEnvValue } from './models/index.mjs'; // Issue #1221, #1978
|
|
25
25
|
import { applyFormalAiPricingOverride } from './formal-ai-pricing.lib.mjs'; // Issue #2119
|
|
26
|
-
import { logPreparedToolCommand,
|
|
26
|
+
import { buildAuthRemedyLines, isPrepareOnly, logPreparedToolCommand, resolveFormalAiToolExecution } from './formal-ai.lib.mjs';
|
|
27
27
|
import { buildMcpConfigWithoutPlaywright, ensureClaudePlaywrightMcpServer } from './playwright-mcp.lib.mjs';
|
|
28
28
|
import { resolveClaudeSessionToolFlags } from './useless-tools.lib.mjs';
|
|
29
29
|
import { ensureClaudeQuietConfig } from './claude-quiet-config.lib.mjs';
|
|
@@ -41,6 +41,8 @@ export { availableModels, fetchModelInfo }; // Re-export for backward compatibil
|
|
|
41
41
|
export { formatNumber, mapModelToId, checkModelVisionCapability };
|
|
42
42
|
export const validateClaudeConnection = async (model = 'haiku') => {
|
|
43
43
|
const mappedModel = mapModelToId(model);
|
|
44
|
+
// Issue #2130: "run claude login" is wrong advice for a Formal-AI-served model.
|
|
45
|
+
const authRemedyLines = buildAuthRemedyLines({ model, vendorRemedy: 'Please run: claude login' });
|
|
44
46
|
const maxRetries = 3;
|
|
45
47
|
const baseDelay = timeouts.retryBaseDelay;
|
|
46
48
|
let retryCount = 0;
|
|
@@ -137,7 +139,7 @@ export const validateClaudeConnection = async (model = 'haiku') => {
|
|
|
137
139
|
if (stderr) await log(` Error: ${stderr.trim()}`, { level: 'error' });
|
|
138
140
|
}
|
|
139
141
|
if (stderr.includes('Please run /login') || (jsonError && jsonError.type === 'forbidden')) {
|
|
140
|
-
|
|
142
|
+
for (const line of authRemedyLines) await log(line, { level: 'error' });
|
|
141
143
|
}
|
|
142
144
|
return false;
|
|
143
145
|
}
|
|
@@ -158,7 +160,7 @@ export const validateClaudeConnection = async (model = 'haiku') => {
|
|
|
158
160
|
}
|
|
159
161
|
await log(`❌ Claude CLI returned error: ${jsonError.type} - ${jsonError.message}`, { level: 'error' });
|
|
160
162
|
if (jsonError.type === 'forbidden') {
|
|
161
|
-
|
|
163
|
+
for (const line of authRemedyLines) await log(line, { level: 'error' });
|
|
162
164
|
}
|
|
163
165
|
return false;
|
|
164
166
|
}
|
|
@@ -613,7 +615,8 @@ export const executeClaudeCommand = async params => {
|
|
|
613
615
|
const progressMonitor = await initProgressMonitoring(argv, { owner, repo, prNumber, $, log }); // works with or without --interactive-mode
|
|
614
616
|
let execCommand;
|
|
615
617
|
const mappedModel = mapModelToId(argv.model);
|
|
616
|
-
|
|
618
|
+
// Issue #2130: Formal AI runs the native CLI against a local Formal AI server (no argv wrapper).
|
|
619
|
+
const toolInvocation = await resolveFormalAiToolExecution({ tool: 'claude', model: argv.model, toolPath: claudePath, workdir: tempDir, log, verbose: argv.verbose, prepareOnly: isPrepareOnly(argv) });
|
|
617
620
|
const resolvedPlanModel = argv.planModel ? mapModelToId(argv.planModel) : undefined; // Issue #1223
|
|
618
621
|
const resolvedSubAgentModel = argv.subAgentModel ? mapClaudeSubAgentModelToEnvValue(argv.subAgentModel) : undefined; // Issue #1978
|
|
619
622
|
const effectiveModel = resolvedPlanModel ? 'opusplan' : mappedModel;
|
|
@@ -633,12 +636,7 @@ export const executeClaudeCommand = async params => {
|
|
|
633
636
|
await log(`🔄 Resuming from session: ${argv.resume}`);
|
|
634
637
|
claudeArgs = `--resume ${argv.resume} ${claudeArgs}`;
|
|
635
638
|
}
|
|
636
|
-
|
|
637
|
-
try {
|
|
638
|
-
claudeWorkLanguage = (await import('./i18n.lib.mjs')).getWorkLocale?.() ?? null;
|
|
639
|
-
} catch {
|
|
640
|
-
/* ignore */
|
|
641
|
-
}
|
|
639
|
+
const claudeWorkLanguage = await import('./i18n.lib.mjs').then(i18n => i18n.getWorkLocale?.() ?? null).catch(() => null);
|
|
642
640
|
await ensureClaudeQuietConfig({ log, workLanguage: claudeWorkLanguage });
|
|
643
641
|
const { mcpConfigPath, disallowedToolsList } = await resolveClaudeSessionToolFlags({ argv, log, fallbackBuildMcpConfigWithoutPlaywright: buildMcpConfigWithoutPlaywright });
|
|
644
642
|
if (mcpConfigPath) claudeArgs += ` --strict-mcp-config --mcp-config "${mcpConfigPath}"`;
|
|
@@ -660,7 +658,8 @@ export const executeClaudeCommand = async params => {
|
|
|
660
658
|
const { thinkingBudget: resolvedThinkingBudget, thinkLevel, isNewVersion, maxBudget } = await resolveThinkingSettings(argv, log);
|
|
661
659
|
const { parsed: parsedSubSessionSize, contextWindowTokens } = await resolveSubSessionSize({ rawValue: argv.subSessionSize, tool: 'claude', modelId: effectiveModel, fetchModelInfo, log });
|
|
662
660
|
// Issue #817: streaming mode sets exitAfterStopDelayMs=60000 so the headless Claude process stays alive between NDJSON turns.
|
|
663
|
-
|
|
661
|
+
// Issue #2130: `toolInvocation.env` points the native CLI at the local Formal AI server (base URL + API key).
|
|
662
|
+
const claudeEnv = { ...getClaudeEnv({ thinkingBudget: resolvedThinkingBudget, model: effectiveModel, thinkLevel, maxBudget, planModel: resolvedPlanModel, executionModel: resolvedExecutionModel, subAgentModel: resolvedSubAgentModel, showThinkingContent: argv.showThinkingContent, exitAfterStopDelayMs: streamingInput ? 60_000 : undefined, disable1mContext: !!argv.disable1mContext, subSessionSize: parsedSubSessionSize, contextWindowTokens }), ...toolInvocation.env };
|
|
664
663
|
if (argv.verbose) claudeEnv.ANTHROPIC_LOG = 'debug';
|
|
665
664
|
const modelMaxOutputTokens = getMaxOutputTokensForModel(effectiveModel);
|
|
666
665
|
if (argv.verbose) {
|
|
@@ -682,15 +681,15 @@ export const executeClaudeCommand = async params => {
|
|
|
682
681
|
if (useClaudeFallbackModel && argv.verbose) await log(`📊 Claude --fallback-model: ${mappedFallbackModel} (Issue #1949 — primary --model ${effectiveModel} stays stable across overload retries)`, { verbose: true });
|
|
683
682
|
if (argv.resume) {
|
|
684
683
|
const simpleEscapedPrompt = promptForAttempt.replace(/"/g, '\\"');
|
|
685
|
-
execCommand =
|
|
684
|
+
execCommand = $({ cwd: tempDir, mirror: false, env: claudeEnv })`${toolInvocation.command} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${fallbackModelArgs} ${mcpDisableArgs} ${disallowedToolsArgs} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
|
|
686
685
|
} else if (streamingInput) {
|
|
687
686
|
// Issue #817: Drive Claude via --input-format stream-json on a pipe
|
|
688
687
|
// stdin. Initial prompt + later PR comments are written as NDJSON
|
|
689
688
|
// frames by attachStreamingInput (see bidirectional-interactive.lib.mjs).
|
|
690
689
|
const streamingInputArgs = ['-p', '--input-format', 'stream-json'];
|
|
691
|
-
execCommand =
|
|
690
|
+
execCommand = $({ cwd: tempDir, stdin: 'pipe', mirror: false, env: claudeEnv })`${toolInvocation.command} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${fallbackModelArgs} ${mcpDisableArgs} ${disallowedToolsArgs} ${streamingInputArgs} --append-system-prompt "${simpleEscapedSystem}"`;
|
|
692
691
|
} else {
|
|
693
|
-
execCommand =
|
|
692
|
+
execCommand = $({ cwd: tempDir, stdin: promptForAttempt, mirror: false, env: claudeEnv })`${toolInvocation.command} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${fallbackModelArgs} ${mcpDisableArgs} ${disallowedToolsArgs} --append-system-prompt "${simpleEscapedSystem}"`;
|
|
694
693
|
}
|
|
695
694
|
if (streamingInput) {
|
|
696
695
|
await attachStreamingInput(bidirectionalHandler, execCommand, promptForAttempt, log, !!argv.verbose);
|
package/src/codex.lib.mjs
CHANGED
|
@@ -27,12 +27,13 @@ const __codexBuildSolveResumeCmd = (argv, sessionId, tempDir) => (sessionId && a
|
|
|
27
27
|
import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
|
|
28
28
|
import { createLineBuffer } from './json-stream.lib.mjs'; // Issue #2119
|
|
29
29
|
import { mapModelToId, resolveCodexReasoningEffort } from './codex.options.lib.mjs';
|
|
30
|
+
import { buildCodexRunDiagnostics, codexRunAlreadyFailed, describeCodexLastMessageOutcome } from './codex.run-diagnostics.lib.mjs'; // Issue #2130
|
|
30
31
|
import { createInteractiveHandler } from './interactive-mode.lib.mjs';
|
|
31
32
|
import { initProgressMonitoring } from './solve.progress-monitoring.lib.mjs';
|
|
32
33
|
import { ensureCodexPlaywrightMcpServer, getCodexPlaywrightMcpDisableConfigArgs } from './playwright-mcp.lib.mjs';
|
|
33
34
|
import { fetchModelInfo } from './model-info.lib.mjs';
|
|
34
35
|
import { defaultModels, isFormalAiModel } from './models/index.mjs';
|
|
35
|
-
import { logPreparedToolCommand,
|
|
36
|
+
import { buildAuthRemedyLines, buildFormalAiEnvExports, isPrepareOnly, logPreparedToolCommand, resolveFormalAiToolExecution } from './formal-ai.lib.mjs';
|
|
36
37
|
import { buildFormalAiPricingInfo } from './formal-ai-pricing.lib.mjs'; // Issue #2119
|
|
37
38
|
import { classifyRetryableError, prepareRetryAfterError, waitWithCountdown } from './tool-retry.lib.mjs';
|
|
38
39
|
import { parseSubSessionSize, buildCodexSubSessionSizeConfigArgs, buildCodexDisable1mContextConfigArgs } from './sub-session-size.lib.mjs'; // Issue #1706
|
|
@@ -641,7 +642,7 @@ export const validateCodexConnection = async (model = defaultModels.codex, verbo
|
|
|
641
642
|
const authError = new Error('Codex authentication failed - 401 Unauthorized');
|
|
642
643
|
authError.isAuthError = true;
|
|
643
644
|
await log('❌ Codex authentication failed', { level: 'error' });
|
|
644
|
-
|
|
645
|
+
for (const line of buildAuthRemedyLines({ model, vendorRemedy: 'Please run: codex login' })) await log(line, { level: 'error' });
|
|
645
646
|
throw authError;
|
|
646
647
|
}
|
|
647
648
|
|
|
@@ -823,17 +824,17 @@ export const executeCodexCommand = async params => {
|
|
|
823
824
|
|
|
824
825
|
let execCommand;
|
|
825
826
|
const mappedModel = mapModelToId(argv.model);
|
|
826
|
-
const toolInvocation = resolveFormalAiToolInvocation({
|
|
827
|
-
tool: 'codex',
|
|
828
|
-
model: argv.model,
|
|
829
|
-
toolPath: codexPath,
|
|
830
|
-
});
|
|
831
827
|
const { reasoningEffort, source: reasoningEffortSource, rolloutTokenBudget } = resolveCodexReasoningEffort(argv);
|
|
832
828
|
const isResumeMode = !!argv.resume;
|
|
833
829
|
const codexEnv = applyCodexCapabilityEnv(capabilityPreflight?.codexBaseEnv || getCodexExecEnv(argv.verbose), {
|
|
834
830
|
codexHome: capabilityPreflight?.codexHome,
|
|
835
831
|
baseCodexHome: capabilityPreflight?.baseCodexHome,
|
|
836
832
|
});
|
|
833
|
+
// Issue #2130: run the native CLI against a local Formal AI server (no argv wrapper); `codexEnv` seeds the isolated CODEX_HOME.
|
|
834
|
+
const toolInvocation = await resolveFormalAiToolExecution({ tool: 'codex', model: argv.model, toolPath: codexPath, workdir: tempDir, log, verbose: argv.verbose, prepareOnly: isPrepareOnly(argv), env: codexEnv });
|
|
835
|
+
// Issue #2130: "run codex login" is wrong advice for a Formal-AI-served model.
|
|
836
|
+
const codexAuthRemedyLines = buildAuthRemedyLines({ model: argv.model, vendorRemedy: 'Please run: codex login' });
|
|
837
|
+
Object.assign(codexEnv, toolInvocation.env);
|
|
837
838
|
|
|
838
839
|
// For Codex, we combine system and user prompts into a single message
|
|
839
840
|
// Codex doesn't have separate system prompt support in CLI mode
|
|
@@ -901,7 +902,9 @@ export const executeCodexCommand = async params => {
|
|
|
901
902
|
if (subSessionSizeArgs.length) await log(`📊 Codex --sub-session-size: ${subSessionSizeArgs.join(' ')}`, { verbose: true });
|
|
902
903
|
}
|
|
903
904
|
|
|
904
|
-
|
|
905
|
+
// Issue #2130: re-export the Formal AI environment inside the `sh -lc` script so a
|
|
906
|
+
// stale `formal-ai with --global` block in the operator profile cannot override it.
|
|
907
|
+
const fullCommand = `(${buildFormalAiEnvExports(toolInvocation.env)}cd ${shellQuote(tempDir)} && cat ${shellQuote(promptFile)} | ${toolInvocation.displayCommand} ${codexArgs})`;
|
|
905
908
|
|
|
906
909
|
const preparedResult = await logPreparedToolCommand({ argv, fullCommand, log, formatAligned });
|
|
907
910
|
if (preparedResult) return preparedResult;
|
|
@@ -1031,7 +1034,7 @@ export const executeCodexCommand = async params => {
|
|
|
1031
1034
|
authError = true;
|
|
1032
1035
|
await log('\n❌ Authentication error detected in Codex JSON stream', { level: 'error' });
|
|
1033
1036
|
await log(' This error cannot be resolved by retrying.', { level: 'error' });
|
|
1034
|
-
|
|
1037
|
+
for (const line of codexAuthRemedyLines) await log(line, { level: 'error' });
|
|
1035
1038
|
}
|
|
1036
1039
|
}
|
|
1037
1040
|
|
|
@@ -1066,76 +1069,32 @@ export const executeCodexCommand = async params => {
|
|
|
1066
1069
|
authError = true;
|
|
1067
1070
|
await log('\n❌ Authentication error detected in Codex JSON stream', { level: 'error' });
|
|
1068
1071
|
await log(' This error cannot be resolved by retrying.', { level: 'error' });
|
|
1069
|
-
|
|
1072
|
+
for (const line of codexAuthRemedyLines) await log(line, { level: 'error' });
|
|
1070
1073
|
}
|
|
1071
1074
|
|
|
1072
1075
|
if (interactiveHandler) {
|
|
1073
1076
|
await interactiveHandler.flush();
|
|
1074
1077
|
}
|
|
1075
1078
|
|
|
1079
|
+
// Issue #2130: a failed run legitimately has no final message and no
|
|
1080
|
+
// turn.completed usage, so those outcomes must not be logged as warnings.
|
|
1081
|
+
const runFailed = codexRunAlreadyFailed({ state: codexJsonState, exitCode });
|
|
1082
|
+
let lastMessageFromFile = null;
|
|
1083
|
+
let lastMessageReadError = null;
|
|
1076
1084
|
try {
|
|
1077
|
-
|
|
1078
|
-
if (lastMessageFromFile) {
|
|
1079
|
-
await log(`📝 Final Codex message captured in ${lastMessageFile}`, { verbose: true });
|
|
1080
|
-
await log(lastMessageFromFile, { verbose: true });
|
|
1081
|
-
lastTextContent = lastTextContent || lastMessageFromFile;
|
|
1082
|
-
} else {
|
|
1083
|
-
await log(`⚠️ Final Codex message file was empty: ${lastMessageFile}`, { level: 'warning', verbose: true });
|
|
1084
|
-
}
|
|
1085
|
+
lastMessageFromFile = (await fs.readFile(lastMessageFile, 'utf8')).trim();
|
|
1085
1086
|
} catch (readError) {
|
|
1086
|
-
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
if (Object.keys(codexJsonState.eventCounts).length > 0) {
|
|
1090
|
-
const eventSummary = Object.entries(codexJsonState.eventCounts)
|
|
1091
|
-
.map(([eventType, count]) => `${eventType}=${count}`)
|
|
1092
|
-
.join(', ');
|
|
1093
|
-
await log(`📊 Codex JSON events: ${eventSummary}`, { verbose: true });
|
|
1094
|
-
}
|
|
1095
|
-
if (Object.keys(codexJsonState.itemTypeCounts).length > 0) {
|
|
1096
|
-
const itemSummary = Object.entries(codexJsonState.itemTypeCounts)
|
|
1097
|
-
.map(([itemType, count]) => `${itemType}=${count}`)
|
|
1098
|
-
.join(', ');
|
|
1099
|
-
await log(`📦 Codex item types: ${itemSummary}`, { verbose: true });
|
|
1100
|
-
}
|
|
1101
|
-
if (codexJsonState.tokenUsage.stepCount > 0) {
|
|
1102
|
-
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 });
|
|
1103
|
-
} else {
|
|
1104
|
-
await log('📈 No Codex usage found in turn.completed events', { level: 'warning', verbose: true });
|
|
1105
|
-
}
|
|
1106
|
-
if (codexJsonState.subAgentCalls.length > 0) {
|
|
1107
|
-
await log(`🤝 Codex collab/sub-agent calls observed: ${codexJsonState.subAgentCalls.length}`, { verbose: true });
|
|
1087
|
+
lastMessageReadError = readError;
|
|
1108
1088
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1089
|
+
const lastMessageOutcome = describeCodexLastMessageOutcome({ lastMessageFile, lastMessage: lastMessageFromFile, readError: lastMessageReadError, runFailed });
|
|
1090
|
+
await log(lastMessageOutcome.message, lastMessageOutcome.options);
|
|
1091
|
+
if (lastMessageFromFile) {
|
|
1092
|
+
await log(lastMessageFromFile, { verbose: true });
|
|
1093
|
+
lastTextContent = lastTextContent || lastMessageFromFile;
|
|
1111
1094
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
if (codexJsonState.fileChanges.length > 0) {
|
|
1116
|
-
await log(`📝 Codex file change items observed: ${codexJsonState.fileChanges.length}`, { verbose: true });
|
|
1117
|
-
}
|
|
1118
|
-
if (codexJsonState.mcpToolCalls.length > 0) {
|
|
1119
|
-
await log(`🔌 Codex MCP tool calls observed: ${codexJsonState.mcpToolCalls.length}`, { verbose: true });
|
|
1120
|
-
}
|
|
1121
|
-
if (codexJsonState.webSearches.length > 0) {
|
|
1122
|
-
await log(`🌐 Codex web searches observed: ${codexJsonState.webSearches.length}`, { verbose: true });
|
|
1123
|
-
}
|
|
1124
|
-
if (codexJsonState.todoLists.length > 0) {
|
|
1125
|
-
const latestTodoCount = codexJsonState.todoLists.at(-1)?.items?.length || 0;
|
|
1126
|
-
await log(`📋 Codex todo list updates observed: ${codexJsonState.todoLists.length} (latest: ${latestTodoCount} items)`, { verbose: true });
|
|
1127
|
-
}
|
|
1128
|
-
if (codexJsonState.itemErrors.length > 0 || codexJsonState.turnFailures.length > 0 || codexJsonState.streamErrors.length > 0) {
|
|
1129
|
-
await log(`⚠️ Codex error events observed: item=${codexJsonState.itemErrors.length}, turn=${codexJsonState.turnFailures.length}, stream=${codexJsonState.streamErrors.length}`, { verbose: true });
|
|
1130
|
-
}
|
|
1131
|
-
if (codexJsonState.observedUsageFieldSets.length > 0) {
|
|
1132
|
-
const lastUsageFieldSet = codexJsonState.observedUsageFieldSets.at(-1);
|
|
1133
|
-
await log(`📐 Codex usage fields observed: ${lastUsageFieldSet.join(', ')}`, { verbose: true });
|
|
1134
|
-
}
|
|
1135
|
-
if (codexJsonState.observedModelDiagnosticPaths.length > 0) {
|
|
1136
|
-
await log(`🔎 Undocumented model-related JSON fields observed but ignored for accounting: ${codexJsonState.observedModelDiagnosticPaths.join(', ')}`, { verbose: true });
|
|
1137
|
-
} else {
|
|
1138
|
-
await log(`🤖 Codex exec JSON did not expose model IDs; using requested model for reporting: ${mappedModel}`, { verbose: true });
|
|
1095
|
+
|
|
1096
|
+
for (const line of buildCodexRunDiagnostics({ state: codexJsonState, exitCode, mappedModel })) {
|
|
1097
|
+
await log(line.message, line.options);
|
|
1139
1098
|
}
|
|
1140
1099
|
|
|
1141
1100
|
const baseBranchIntervention = baseBranchCommandIntervention.getIntervention();
|
|
@@ -1195,7 +1154,7 @@ export const executeCodexCommand = async params => {
|
|
|
1195
1154
|
await logCodexResourceSnapshot({ getResourceSnapshot, log });
|
|
1196
1155
|
|
|
1197
1156
|
// Throw an error to stop retries and propagate the auth failure
|
|
1198
|
-
const error = new Error(
|
|
1157
|
+
const error = new Error(`Codex authentication failed - 401 Unauthorized.${codexAuthRemedyLines.map(line => ` ${line.replace(/^\s*💡\s*/, '')}`).join('')}`);
|
|
1199
1158
|
error.isAuthError = true;
|
|
1200
1159
|
throw error;
|
|
1201
1160
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Post-run diagnostics for a `codex exec --json` stream (issue #2130).
|
|
5
|
+
*
|
|
6
|
+
* Split out of codex.lib.mjs to keep that file under the max-lines budget, and
|
|
7
|
+
* kept pure so the "is this line a real warning?" decisions are unit-testable.
|
|
8
|
+
*
|
|
9
|
+
* The motivating defect: when a run already failed (`turn.failed`, non-zero
|
|
10
|
+
* exit), Codex never writes the `--output-last-message` file and never emits a
|
|
11
|
+
* `turn.completed` usage block. Hive Mind reported both of those *expected*
|
|
12
|
+
* consequences as WARNINGs, so the log for a single upstream failure carried two
|
|
13
|
+
* extra warnings that pointed at nothing actionable:
|
|
14
|
+
*
|
|
15
|
+
* [WARNING] ⚠️ Could not read Codex final message file: ENOENT: no such file …
|
|
16
|
+
* [WARNING] 📈 No Codex usage found in turn.completed events
|
|
17
|
+
*
|
|
18
|
+
* See docs/case-studies/issue-2130 (data/tool-logs/codex-02-rv2k7W.log.gz:752).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Did this Codex run already report a failure of its own?
|
|
23
|
+
*
|
|
24
|
+
* @param {object} params
|
|
25
|
+
* @param {object} [params.state] - accumulated parseCodexExecJsonOutput state.
|
|
26
|
+
* @param {number|null} [params.exitCode] - the CLI exit code, if known.
|
|
27
|
+
* @returns {boolean}
|
|
28
|
+
*/
|
|
29
|
+
export const codexRunAlreadyFailed = ({ state = {}, exitCode = null } = {}) => {
|
|
30
|
+
if (typeof exitCode === 'number' && exitCode !== 0) return true;
|
|
31
|
+
if (state.authError) return true;
|
|
32
|
+
return !!(state.turnFailures?.length || state.itemErrors?.length || state.streamErrors?.length);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const isMissingFileError = error => error?.code === 'ENOENT' || /ENOENT/.test(error?.message || '');
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Describe the `--output-last-message` file outcome.
|
|
39
|
+
*
|
|
40
|
+
* @param {object} params
|
|
41
|
+
* @param {string} params.lastMessageFile - the path Codex was asked to write.
|
|
42
|
+
* @param {string|null} [params.lastMessage] - trimmed contents, when readable.
|
|
43
|
+
* @param {Error|null} [params.readError] - the read failure, when unreadable.
|
|
44
|
+
* @param {boolean} [params.runFailed] - result of codexRunAlreadyFailed.
|
|
45
|
+
* @returns {{message: string, options: object}} a single log line.
|
|
46
|
+
*/
|
|
47
|
+
export const describeCodexLastMessageOutcome = ({ lastMessageFile, lastMessage = null, readError = null, runFailed = false }) => {
|
|
48
|
+
if (readError) {
|
|
49
|
+
// A failed run never gets far enough to write the file; saying so at
|
|
50
|
+
// warning level invents a second problem on top of the reported one.
|
|
51
|
+
if (isMissingFileError(readError) && runFailed) {
|
|
52
|
+
return { message: '📝 Codex wrote no final message file (the run ended with an error)', options: { verbose: true } };
|
|
53
|
+
}
|
|
54
|
+
return { message: `⚠️ Could not read Codex final message file: ${readError.message}`, options: { level: 'warning', verbose: true } };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (lastMessage) {
|
|
58
|
+
return { message: `📝 Final Codex message captured in ${lastMessageFile}`, options: { verbose: true } };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (runFailed) {
|
|
62
|
+
return { message: '📝 Codex left the final message file empty (the run ended with an error)', options: { verbose: true } };
|
|
63
|
+
}
|
|
64
|
+
return { message: `⚠️ Final Codex message file was empty: ${lastMessageFile}`, options: { level: 'warning', verbose: true } };
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build every verbose diagnostic line for a finished Codex run.
|
|
69
|
+
*
|
|
70
|
+
* @param {object} params
|
|
71
|
+
* @param {object} params.state - accumulated parseCodexExecJsonOutput state.
|
|
72
|
+
* @param {number|null} [params.exitCode] - the CLI exit code, if known.
|
|
73
|
+
* @param {string} params.mappedModel - the model id used for reporting.
|
|
74
|
+
* @returns {Array<{message: string, options: object}>} lines in log order.
|
|
75
|
+
*/
|
|
76
|
+
export const buildCodexRunDiagnostics = ({ state = {}, exitCode = null, mappedModel = null } = {}) => {
|
|
77
|
+
const runFailed = codexRunAlreadyFailed({ state, exitCode });
|
|
78
|
+
const lines = [];
|
|
79
|
+
const push = (message, options = { verbose: true }) => lines.push({ message, options });
|
|
80
|
+
|
|
81
|
+
const counts = pairs =>
|
|
82
|
+
Object.entries(pairs || {})
|
|
83
|
+
.map(([key, count]) => `${key}=${count}`)
|
|
84
|
+
.join(', ');
|
|
85
|
+
|
|
86
|
+
if (Object.keys(state.eventCounts || {}).length > 0) push(`📊 Codex JSON events: ${counts(state.eventCounts)}`);
|
|
87
|
+
if (Object.keys(state.itemTypeCounts || {}).length > 0) push(`📦 Codex item types: ${counts(state.itemTypeCounts)}`);
|
|
88
|
+
|
|
89
|
+
const usage = state.tokenUsage || {};
|
|
90
|
+
if (usage.stepCount > 0) {
|
|
91
|
+
push(`📈 Codex usage from turn.completed: ${usage.inputTokens.toLocaleString()} input, ${usage.cacheReadTokens.toLocaleString()} cache read, ${usage.outputTokens.toLocaleString()} output across ${usage.stepCount} turn(s)`);
|
|
92
|
+
} else if (runFailed) {
|
|
93
|
+
// No `turn.completed` is the definition of a failed turn, not a second fault.
|
|
94
|
+
push('📈 Codex reported no usage (the turn never completed)');
|
|
95
|
+
} else {
|
|
96
|
+
push('📈 No Codex usage found in turn.completed events', { level: 'warning', verbose: true });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (state.subAgentCalls?.length > 0) push(`🤝 Codex collab/sub-agent calls observed: ${state.subAgentCalls.length}`);
|
|
100
|
+
if (state.reasoningSummaries?.length > 0) push(`🧠 Codex reasoning summaries observed: ${state.reasoningSummaries.length}`);
|
|
101
|
+
if (state.commandExecutions?.length > 0) push(`💻 Codex command executions observed: ${state.commandExecutions.length}`);
|
|
102
|
+
if (state.fileChanges?.length > 0) push(`📝 Codex file change items observed: ${state.fileChanges.length}`);
|
|
103
|
+
if (state.mcpToolCalls?.length > 0) push(`🔌 Codex MCP tool calls observed: ${state.mcpToolCalls.length}`);
|
|
104
|
+
if (state.webSearches?.length > 0) push(`🌐 Codex web searches observed: ${state.webSearches.length}`);
|
|
105
|
+
if (state.todoLists?.length > 0) push(`📋 Codex todo list updates observed: ${state.todoLists.length} (latest: ${state.todoLists.at(-1)?.items?.length || 0} items)`);
|
|
106
|
+
if (state.itemErrors?.length || state.turnFailures?.length || state.streamErrors?.length) {
|
|
107
|
+
push(`⚠️ Codex error events observed: item=${state.itemErrors?.length || 0}, turn=${state.turnFailures?.length || 0}, stream=${state.streamErrors?.length || 0}`);
|
|
108
|
+
}
|
|
109
|
+
if (state.observedUsageFieldSets?.length > 0) push(`📐 Codex usage fields observed: ${state.observedUsageFieldSets.at(-1).join(', ')}`);
|
|
110
|
+
if (state.observedModelDiagnosticPaths?.length > 0) {
|
|
111
|
+
push(`🔎 Undocumented model-related JSON fields observed but ignored for accounting: ${state.observedModelDiagnosticPaths.join(', ')}`);
|
|
112
|
+
} else {
|
|
113
|
+
push(`🤖 Codex exec JSON did not expose model IDs; using requested model for reporting: ${mappedModel}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return lines;
|
|
117
|
+
};
|