fix(agents): cron and agent runs mark successful exec commands with semantic nonzero exits as failed#111574
Conversation
…tool failures The exec runtime marks normal process exits as completed and preserves stdout, but isToolResultError still flagged any finite nonzero exitCode as an error. Completed exec results with semantic exit codes such as grep exiting 1 on no match were pushed into isError envelopes, false Exec failed warnings, and cron run failure diagnostics. The classifier now defers to an explicit completed status. Failure statuses, ok or success false, error fields, timedOut, and statusless nonzero exits still classify as errors.
|
Codex review: needs maintainer review before merge. Reviewed July 19, 2026, 9:55 PM ET / July 20, 2026, 01:55 UTC. Summary PR surface: Source +3, Tests +34. Total +37 across 3 files. Reproducibility: yes. the supplied controlled Gateway cron run gives a concrete before/after path, and the changed classifier is directly source-reproducible. The review did not independently rerun that scenario against the current main SHA. Review metrics: none identified. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Maintainer decision needed
Security Review detailsBest possible solution: Confirm that structured Do we have a high-confidence way to reproduce the issue? Yes, the supplied controlled Gateway cron run gives a concrete before/after path, and the changed classifier is directly source-reproducible. The review did not independently rerun that scenario against the current main SHA. Is this the best way to solve the issue? Unclear pending maintainer intent: the patch is the narrowest repair if completed status is the shared success authority, but an exec-owned boundary is safer if that status is not a generic contract. AGENTS.md: unclear because the file could not be read completely. Codex review notes: model internal, reasoning high; reviewed against 27f05c8993fb. Label changesLabel justifications:
Evidence reviewedPR surface: Source +3, Tests +34. Total +37 across 3 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
Fixes #97318
Related: #107278 and #110153 (same root cause), #94846 (downstream cron delivery handling), #110244 (adjacent open PR for zero-exit false positives; it does not change nonzero-exit classification), #107283 (prior attempt with the same classifier change, closed by its author after review noted missing live behavior proof)
What Problem This Solves
Fixes an issue where agent and isolated cron runs treat a successfully completed exec command with a semantic nonzero exit code (for example grep exiting 1 on no match, or an audit script exiting 1 to mean "alert needed") as a tool failure. The exec runtime already returns these results as status completed with stdout preserved, but the shared tool-result classifier still flags any finite nonzero exitCode as an error. That error state flows into embedded-agent isError envelopes, "Exec failed" warnings on chat and TUI progress surfaces, false failure diagnostics persisted on cron run records, and cron outcome resolution, which only rescues such runs through fragile text-prefix matching on the warning payloads.
Why This Change Was Made
Root cause is the final fallback in isToolResultError:
This exit-code fallback was introduced in #93228 and predates the exec runtime's completed/failed split. The exec runtime is the owner of exit semantics: buildExecExitOutcome (src/agents/bash-tools.exec-runtime.ts) marks normal process exits as completed (except shell 126/127) and marks spawn errors, timeouts, signals, denials, and 126/127 as failed with a reason. The classifier was second-guessing that classification.
The guard sits after every failure check, so nothing else weakens: explicit ok/success false, all failure statuses (failed, timeout, denied, killed, and the rest), timedOut, and error fields still classify as errors first, and statusless results with a nonzero exitCode keep the old behavior. resolveToolResultFailureKind inherits the same boundary. This is the repair ClawSweeper recommended on #97318 ("Make normal exec nonzero nonfatal" at the shared classifier), left unimplemented pending proof after #107283 was closed without it.
The issue carries clawsweeper:no-new-fix-pr pending a maintainer product decision between three options. This PR implements the recommended option so the decision has a reviewable, proven candidate; happy to close if maintainers pick a different boundary.
User Impact
Cron jobs and agent turns that use exit codes semantically no longer record false "Exec failed" diagnostics or error-flagged tool payloads, and no longer depend on warning-text prefix heuristics to avoid being marked as failed runs. True failures (command not found, permission denied, timeouts, signals, spawn errors) still classify as failures, including shell 126/127.
Evidence
Live before/after on a real Gateway, driven end to end through the production CLI: isolated OPENCLAW_STATE_DIR, real gateway process on port 19917, a real isolated cron agentTurn job with toolsAllow ["exec"], and the exec tool spawning a real grep subprocess that exits 1 (no match). The only mocked seam is the model provider HTTP endpoint (a local OpenAI Responses server that first returns an exec tool call for
grep CRITICAL service.log, then replies NO_REPLY). The gateway, cron scheduler, embedded agent runner, exec runtime, SQLite state, and cron CLI are all real, and both runs used identical job settings, command, provider stub, and inputs.BEFORE (pristine main 3c0f55b): the run record persists failure diagnostics for the successful no-match scan:
AFTER (this patch, identical inputs): the same run is clean, with no failure diagnostics at all:
The provider stub logged the tool output the model received in both runs, byte-identical:⚠️ 🛠️ "; the false error payload was real and any tool result not matching that prefix would have failed the run, which is the failure mode reported in #97318 and #94846.
"\n\n(Command exited with code 1)". This confirms the lower exec layer already preserves output on main and the remaining defect was classification only. The BEFORE run only avoided status "error" because resolveCronPayloadOutcome (src/cron/isolated-agent/helpers.ts) special-cases payload text starting with the literal prefix "Verification
node scripts/run-vitest.mjs src/agents/tool-result-error.test.ts src/agents/embedded-agent-subscribe.tools.test.ts src/agents/embedded-agent-runner/extensions.test.ts src/agents/embedded-agent-runner/cli-backend-dispatch.test.ts src/agents/harness/tool-result-middleware.test.ts src/agents/embedded-agent-message-tool-source-reply.test.ts src/cron/run-diagnostics.test.ts src/agents/bash-tools.test.ts: all pass.node scripts/run-oxlint.mjsandoxfmt --checkon the changed files: clean.