Bug: isolated agentTurn cron jobs fail when exec tool returns exit code 1, even when the agent recovers and produces correct output
Version: 2026.6.8 (844f405)
Component: Cron / isolated agentTurn runner
Summary
An isolated agentTurn cron job is marked as status: "error" when an exec tool call returns a non-zero exit code, even when:
- The session itself completes with
finalStatus: "success"
- The agent successfully interprets the exit code as a signal (not an error), produces correct output, and the run is functionally correct
- The cron prompt explicitly instructs the agent to treat exit 1 as a meaningful result
This breaks the established pattern of using exit 1 as a "work to do" signal in cron scripts that the agent then acts on.
Reproduction
Configure an agentTurn cron job whose prompt instructs:
Run: python3 /path/to/check-script.py
If exit 0: log OK.
If exit 1: include the stdout output in your reply (it contains an update command).
The script exits 1 when it has something to report (e.g. an upstream update is available). The agent reads the output and produces a correct, useful reply. The trajectory shows finalStatus: "success" and the agent's assistantTexts contain the expected output.
Expected: cron.state.lastRunStatus = "ok"
Actual: cron.state.lastRunStatus = "error" with lastError: "⚠️ 🛠️ \run python3 /path/to/check-script.py` failed"`
Evidence from trajectory
Trajectory for a real failing run (f5ec7832-f63c-47a8-9a15-7f4351240a91):
// session.completed event
{
"type": "session.completed",
"data": {
"finalStatus": "success",
"aborted": false,
"timedOut": false,
"assistantTexts": [
"I'll check for upstream updates to both Superpowers and last30days.",
"**Upstream Updates Summary:**\n\n**Superpowers v6.0.3 available** ..."
],
"lastToolError": {
"toolName": "exec",
"meta": "run python3 ~/.openclaw/workspace/skills/superpowers-sync/scripts/superpowers-update-check.py",
"error": ":zap: *obra/superpowers* has a new release: *v6.0.3* (`v6.0.3`)",
"mutatingAction": true,
"actionFingerprint": "..."
}
}
}
Despite finalStatus: "success" and correct assistantTexts, the cron runtime surfaces lastToolError as the job-level error.
Root cause (traced through source)
In helpers-CYQZyDV5.js, resolveCronPayloadOutcome() determines hasFatalErrorPayload:
const hasFatalStructuredErrorPayload =
hasErrorPayload &&
!hasSuccessfulPayloadAfterLastError &&
!hasPendingPresentationWarning &&
!hasNonTerminalToolErrorWarning &&
!hasRecoveredToolWarning;
The hasRecoveredToolWarning escape hatch requires both:
isCronToolWarning(payload.text) → true (the ⚠️ 🛠️ prefix matches ✅)
preferFinalAssistantVisibleText === true (from resolveCronChannelOutputPolicy)
The problem is condition 2. preferFinalAssistantVisibleText is only true when the delivery channel plugin sets outbound.preferFinalAssistantVisibleText = true. For Slack, this appears to be false (or absent), so the escape hatch never fires. The tool error becomes fatal regardless of whether the agent recovered.
The result: a cron job where the agent correctly handles a non-zero exit code and produces valid output is nonetheless marked as failed, fires a failureAlert, and increments consecutiveErrors.
Impact
- Affects any isolated
agentTurn cron that runs a script using exit 1 as a "signal" rather than an "error"
- Common pattern in script-driven crons: exit 0 = nothing to do, exit 1 = work to report, exit 2 = send a notification
- The cron fires correct Slack notifications via the agent's text reply (via
delivery.mode: "announce"), while simultaneously being marked as error and triggering failureAlert noise
consecutiveErrors increments on every successful run, eventually triggering alerts after N errors even though nothing is broken
Affected real-world jobs in our setup: upstream-update-checks-12h (9 consecutive false errors), daily-ap-followup (3 false errors), token-watchdog (1 false error).
Expected behavior
If the agent successfully produces output in response to a tool's non-zero exit (i.e. finalStatus: "success", non-empty assistantTexts, and the error payload is a ⚠️ 🛠️-prefixed tool warning), the cron run should be classified as ok, not error.
Specifically: hasRecoveredToolWarning should not require preferFinalAssistantVisibleText to be set by the channel plugin. When finalAssistantVisibleText exists and all error payloads pass isCronToolWarning(), the warning should be treated as non-fatal regardless of delivery channel.
Workaround
Currently none clean. Possible awkward workarounds:
- Rewrite all scripts to use only exit 0 (no output) and exit 2 (notify), never exit 1 — but this conflicts with standard Unix convention and requires rewriting many existing scripts
- Redirect exit-1 text to stderr (not picked up as an error payload) — brittle
Environment
- macOS 26.5.1 (arm64), Node v24.16.0
- OpenClaw 2026.6.8 (844f405)
- Delivery channel: Slack (
mode: "announce", channel: "slack")
- Session target:
isolated
- Payload kind:
agentTurn
- Model:
anthropic/claude-haiku-4-5-20251001
Bug: isolated agentTurn cron jobs fail when exec tool returns exit code 1, even when the agent recovers and produces correct output
Version: 2026.6.8 (844f405)
Component: Cron / isolated agentTurn runner
Summary
An isolated
agentTurncron job is marked asstatus: "error"when anexectool call returns a non-zero exit code, even when:finalStatus: "success"This breaks the established pattern of using
exit 1as a "work to do" signal in cron scripts that the agent then acts on.Reproduction
Configure an
agentTurncron job whose prompt instructs:The script exits 1 when it has something to report (e.g. an upstream update is available). The agent reads the output and produces a correct, useful reply. The trajectory shows
finalStatus: "success"and the agent'sassistantTextscontain the expected output.Expected:
cron.state.lastRunStatus = "ok"Actual:
cron.state.lastRunStatus = "error"withlastError: "⚠️ 🛠️ \run python3 /path/to/check-script.py` failed"`Evidence from trajectory
Trajectory for a real failing run (
f5ec7832-f63c-47a8-9a15-7f4351240a91):Despite
finalStatus: "success"and correctassistantTexts, the cron runtime surfaceslastToolErroras the job-level error.Root cause (traced through source)
In
helpers-CYQZyDV5.js,resolveCronPayloadOutcome()determineshasFatalErrorPayload:The
hasRecoveredToolWarningescape hatch requires both:isCronToolWarning(payload.text)→ true (the⚠️ 🛠️prefix matches ✅)preferFinalAssistantVisibleText === true(fromresolveCronChannelOutputPolicy)The problem is condition 2.
preferFinalAssistantVisibleTextis onlytruewhen the delivery channel plugin setsoutbound.preferFinalAssistantVisibleText = true. For Slack, this appears to befalse(or absent), so the escape hatch never fires. The tool error becomes fatal regardless of whether the agent recovered.The result: a cron job where the agent correctly handles a non-zero exit code and produces valid output is nonetheless marked as failed, fires a
failureAlert, and incrementsconsecutiveErrors.Impact
agentTurncron that runs a script usingexit 1as a "signal" rather than an "error"delivery.mode: "announce"), while simultaneously being marked aserrorand triggeringfailureAlertnoiseconsecutiveErrorsincrements on every successful run, eventually triggering alerts after N errors even though nothing is brokenAffected real-world jobs in our setup:
upstream-update-checks-12h(9 consecutive false errors),daily-ap-followup(3 false errors),token-watchdog(1 false error).Expected behavior
If the agent successfully produces output in response to a tool's non-zero exit (i.e.
finalStatus: "success", non-emptyassistantTexts, and the error payload is a⚠️ 🛠️-prefixed tool warning), the cron run should be classified asok, noterror.Specifically:
hasRecoveredToolWarningshould not requirepreferFinalAssistantVisibleTextto be set by the channel plugin. WhenfinalAssistantVisibleTextexists and all error payloads passisCronToolWarning(), the warning should be treated as non-fatal regardless of delivery channel.Workaround
Currently none clean. Possible awkward workarounds:
Environment
mode: "announce",channel: "slack")isolatedagentTurnanthropic/claude-haiku-4-5-20251001