Bug type
Reliability / silent-turn regression
Summary
Isolated cron runs with payload.kind: "agentTurn" can fail with incomplete turn detected ... payloads=0 even when the latest successful tool result is exact NO_REPLY and the unchanged path should be a quiet success.
The bad path is:
- the assistant makes a tool call
- the tool succeeds and returns exact
NO_REPLY
- the model then emits an empty final assistant message (
stopReason=stop, content: [])
- embedded finalization drops into the incomplete-turn path instead of preserving the silent success
On announce surfaces this can also produce duplicate user-visible failures: the raw embedded error payload plus the cron-layer wrapped failure message.
Version
Observed on OpenClaw 2026.4.15.
I validated the source fix against current main as well.
Minimal repro shape
A narrow cron repro only needs an isolated agentTurn job whose unchanged path is intentionally silent.
Prompt shape:
- use a single tool call (for example
exec)
- the tool returns exact
NO_REPLY on the no-change path
- the prompt tells the assistant: if stdout is exactly
NO_REPLY, reply with only NO_REPLY
Sanitized transcript shape from a failing run:
- user: cron prompt
- assistant: tool call
- tool result:
NO_REPLY
- assistant: final message with
stopReason=stop and content: []
Sanitized gateway log line from the same failure mode:
[agent/embedded] incomplete turn detected: runId=<redacted> sessionId=<redacted> stopReason=stop payloads=0 — surfacing error to user
Expected behavior
When the latest successful tool result is exact NO_REPLY, the cron run should complete as a quiet success.
That means:
- no incomplete-turn error
- no user-visible failure message
- cron delivery treats the run as silent / unchanged
Actual behavior
The embedded runner only considers assistant-derived payloads (plus tool media) during finalization.
If the assistant ends with an empty final message after the successful NO_REPLY tool result, the run reaches payloadCount=0 and is treated as an incomplete turn.
That converts a valid quiet-success path into an error.
Root-cause seam
The source seam is in embedded runner finalization:
buildEmbeddedRunPayloads(...) returns no assistant payloads
mergeAttemptToolMediaPayloads(...) still leaves no payloads
resolveIncompleteTurnPayloadText(...) sees payloadCount=0
- but the latest successful tool result already contains a valid silent sentinel (
NO_REPLY)
So the runner loses a legitimate silent outcome because it only trusts the final assistant payload path, not the latest successful tool-result path.
Fix shape validated locally
The narrow fix that worked for me was:
- when the trigger is cron
- and final payload count is still zero
- and there is no timeout / abort / tool-error blocker
- inspect the latest successful tool result in
messagesSnapshot
- if that tool result is exact silent
NO_REPLY, synthesize a final silent payload instead of surfacing an incomplete-turn error
This keeps the change scoped to cron silent-success handling and does not change normal visible replies.
Validation
Targeted source-level validation passed:
src/agents/pi-embedded-runner/run.incomplete-turn.test.ts
src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts
The regression test covers the exact shape above:
- latest successful tool result is
NO_REPLY
- final assistant message is empty
- trigger is cron
- final result is quiet success instead of incomplete-turn error
Why I think this should be a separate tracked bug
This is related to earlier cron / NO_REPLY delivery work, but it appears to be a different seam:
- previous fixes were largely about delivery-time stripping / suppression
- this one happens earlier, inside embedded-run finalization, before cron delivery gets a correct silent payload to work with
Bug type
Reliability / silent-turn regression
Summary
Isolated cron runs with
payload.kind: "agentTurn"can fail withincomplete turn detected ... payloads=0even when the latest successful tool result is exactNO_REPLYand the unchanged path should be a quiet success.The bad path is:
NO_REPLYstopReason=stop,content: [])On announce surfaces this can also produce duplicate user-visible failures: the raw embedded error payload plus the cron-layer wrapped failure message.
Version
Observed on OpenClaw
2026.4.15.I validated the source fix against current
mainas well.Minimal repro shape
A narrow cron repro only needs an isolated
agentTurnjob whose unchanged path is intentionally silent.Prompt shape:
exec)NO_REPLYon the no-change pathNO_REPLY, reply with onlyNO_REPLYSanitized transcript shape from a failing run:
NO_REPLYstopReason=stopandcontent: []Sanitized gateway log line from the same failure mode:
Expected behavior
When the latest successful tool result is exact
NO_REPLY, the cron run should complete as a quiet success.That means:
Actual behavior
The embedded runner only considers assistant-derived payloads (plus tool media) during finalization.
If the assistant ends with an empty final message after the successful
NO_REPLYtool result, the run reachespayloadCount=0and is treated as an incomplete turn.That converts a valid quiet-success path into an error.
Root-cause seam
The source seam is in embedded runner finalization:
buildEmbeddedRunPayloads(...)returns no assistant payloadsmergeAttemptToolMediaPayloads(...)still leaves no payloadsresolveIncompleteTurnPayloadText(...)seespayloadCount=0NO_REPLY)So the runner loses a legitimate silent outcome because it only trusts the final assistant payload path, not the latest successful tool-result path.
Fix shape validated locally
The narrow fix that worked for me was:
messagesSnapshotNO_REPLY, synthesize a final silent payload instead of surfacing an incomplete-turn errorThis keeps the change scoped to cron silent-success handling and does not change normal visible replies.
Validation
Targeted source-level validation passed:
src/agents/pi-embedded-runner/run.incomplete-turn.test.tssrc/cron/isolated-agent/delivery-dispatch.double-announce.test.tsThe regression test covers the exact shape above:
NO_REPLYWhy I think this should be a separate tracked bug
This is related to earlier cron /
NO_REPLYdelivery work, but it appears to be a different seam: