Skip to content

[Bug] Cron agentTurn misclassified as fatal when agent recovers from a transient tool error — success announce suppressed #96255

Description

@bigbrother666sh

[Bug] Cron agentTurn run misclassified as fatal when agent recovers from a transient tool error — success announce suppressed

Environment

  • OpenClaw version: 2026.6.8 (commit 844f405ac1be805d5c598922a37254f12ab6d765)
  • Trigger: cron job with payload.kind: "agentTurn", sessionTarget: "isolated", delivery.mode: "announce"
  • Model that produced the run: minimax/MiniMax-M3
  • Host: Linux 6.17.0, Node v24.16.0
  • Observed: every night 2026-06-19 through 2026-06-24, Asia/Shanghai 01:08 cron run

Summary

When an isolated cron agentTurn hits a non-fatal tool error mid-run (e.g. an exec call rejected for an unsupported shell redirect), and the agent recovers by retrying with a different successful action and then emits a final user-visible answer, the cron runner classifies the whole run as status: "error". As a result:

  1. The success announce delivery is skipped entirely — the operator receives no report at all.
  2. A failure notification is synthesized from the recovered tool error (⚠️ 🛠️ <command> failed).

From the operator's perspective the cron "failed every night", but the underlying task actually completed successfully and produced a full report — the report just never got delivered.

Observed evidence

Cron run log (cron_run_logs table):

2026-06-24 01:08 | status=error | error=⚠️ 🛠️ run query.sh xhs → show /tmp/xhs_query.json failed | delivered=0 | delivery_status=not-delivered
2026-06-23 01:08 | status=error | error=⚠️ 🛠️ list files in calibration/wx_mp/predictions/ …      | delivered=0
2026-06-22 01:08 | status=error | error=⚠️ 🛠️ …python one-liner…                                      | delivered=0
…(same shape every night since 06-19)
2026-06-18 01:08 | status=ok    | delivered=1   ← last clean run (no tool error that night)

The 2026-06-24 session trajectory tells the real story:

  • session.ended.data.finalStatus = "success", aborted/timedOut = false
  • The agent's first exec used a shell redirect:
    query.sh --platform xhs --limit 12 > /tmp/xhs_query.json 2>&1; head -c 500 /tmp/xhs_query.json
    
    The exec tool doesn't run a shell, so it rejected this with {"ok":false,"error":"unknown arg: >"}, exitCode: 1. The exec details carries no errorCode field — only exitCode + aggregated.
  • The agent immediately retried without the redirect (query.sh --platform xhs --limit 12) and succeeded, then completed all remaining steps (60+ further tool calls) and emitted a full final report as finalAssistantVisibleText.
  • trace.artifacts.data.lastToolError is still populated by the recovered exec error at run end (see related [Bug] Gateway swallows user-visible reply when an agent's exec stdout matches "configuration file" — replies=2 puts ⚠️ 🛠️ first, the actual reply second #96239 for the same lastToolError/mutatingAction area), but that is a separate symptom — see root cause below.

Root cause

The fatal classification happens in resolveCronPayloadOutcome (src/cron/isolated-agent/helpers.ts). With runLevelError undefined (session succeeded) and failureSignal undefined (the exec error has no errorCode, so resolveEmbeddedRunFailureSignal returns undefined), the run is fatal iff hasFatalStructuredErrorPayload:

// src/cron/isolated-agent/helpers.ts:300-305
const hasFatalStructuredErrorPayload =
    hasErrorPayload &&
    !hasSuccessfulPayloadAfterLastError &&
    !hasPendingPresentationWarning &&
    !hasNonTerminalToolErrorWarning &&
    !hasRecoveredToolWarning;

The recovery gates fail to fire for a recovered exec error:

  1. hasSuccessfulPayloadAfterLastError (:258) only counts deliverable successful payloads after the last error (isSuccessfulCronPayloadisDeliverablePayloadhasOutboundReplyContent). The agent's retry exec succeeds, but its stdout JSON is intermediate tool output, not deliverable, so it doesn't count.
  2. The agent's final report lives in finalAssistantVisibleText, which is not in params.payloads and is not consulted by hasSuccessfulPayloadAfterLastError.
  3. hasNonTerminalToolErrorWarning / hasPendingPresentationWarning / hasRecoveredToolWarning all require the last error payload to carry specific markers (nonTerminalToolErrorWarning metadata, a presentation-warning text shape, or a "tool warning" text classification). A plain exec error like unknown arg: > carries none of these markers, so none of the gates fire.

Net effect: a recovered tool error with a successful final answer is treated as fatal. Then in src/cron/isolated-agent/run.ts:1133:

if (hasFatalStructuredErrorPayload && prepared.deliveryRequested) {
  await cleanupDirectCronSession({...});
  return resolveRunOutcome({...});   // ← returns BEFORE dispatchCronDelivery
}

the success announce is never dispatched (delivered=0), and a failure notification is built from the recovered tool error instead.

Suggested fix

The presence of a non-empty finalAssistantVisibleText (or any non-error tool payload after the last error) should constitute proof of recovery and downgrade hasFatalStructuredErrorPayload to false. Minimal change in resolveCronPayloadOutcome:

const hasRecoveredByFinalAnswer =
  !params.runLevelError &&
  params.failureSignal?.fatalForCron !== true &&
  normalizedFinalAssistantVisibleText !== undefined;

const hasFatalStructuredErrorPayload =
  hasErrorPayload &&
  !hasSuccessfulPayloadAfterLastError &&
  !hasPendingPresentationWarning &&
  !hasNonTerminalToolErrorWarning &&
  !hasRecoveredToolWarning &&
  !hasRecoveredByFinalAnswer;

(Alternatively, broaden hasSuccessfulPayloadAfterLastError to count any payload.isError !== true payload after the last error, not only deliverable ones.)

This preserves the existing behavior for genuinely fatal runs (no final answer, run-level error, or fatalForCron failure signal) while letting a recovered run deliver its final report.

Impact

  • The operator receives no cron report for otherwise-successful runs (silent success, noisy failure).
  • Recurs nightly for any cron whose agent habitually probes with a shell-ism the exec tool rejects (>, 2>&1, ;, quoted python one-liners, etc.) and then retries — a common MiniMax/Claude recovery pattern.
  • Degrades trust in cron status: 6 consecutive nights of status=error that were all actually successful.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.impact:message-lossChannel message delivery can be lost, duplicated, or misrouted.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions