fix(agents): classify embedded prompt lock error as permanent announce failure#92274
Conversation
|
Codex review: needs real behavior proof before merge. Reviewed June 27, 2026, 1:01 PM ET / 17:01 UTC. Summary PR surface: Source +73, Tests +246. Total +319 across 5 files. Reproducibility: yes. at source level: current main lacks the lock-change terminal classifier and only credits delivered announce results, matching the linked duplicate-send report. I did not run the live Telegram race in this read-only review. Review metrics: none identified. Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Mantis proof suggestion Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Keep this PR open as the canonical fix path and land it only after redacted Telegram or equivalent runtime-log proof, unless a maintainer explicitly accepts source proof for this transport risk. Do we have a high-confidence way to reproduce the issue? Yes at source level: current main lacks the lock-change terminal classifier and only credits delivered announce results, matching the linked duplicate-send report. I did not run the live Telegram race in this read-only review. Is this the best way to solve the issue? Yes for the code shape: the merge result distinguishes post-send lock-change failures from pre-send recovery and propagates terminal state through cleanup. The remaining blocker is real behavior proof or explicit maintainer acceptance of the proof gap. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 2720ac06b7c8. Label changesLabel justifications:
Evidence reviewedPR surface: Source +73, Tests +246. Total +319 across 5 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
|
|
@vincentkoc would you mind reviewing this? Thanks! A one-line fix in subagent-announce-delivery.ts: adds the embedded prompt lock error to permanent error patterns, preventing 3x duplicate announce retries. Mirrors the approach from #89812.
|
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
@vincentkoc would you mind reviewing this? Thanks! |
…nounce error Per ClawSweeper review feedback on openclaw#92274: the unconditional permanent-regex approach may prevent duplicates but can also suppress recovery for pre-send lock failures. This commit adds structured send-evidence checks so the session-file- changed takeover error is only treated as permanent when a visible send has already occurred. Changes: - Extract SESSION_FILE_CHANGED_ANNOUNCE_RE as a named constant - Add hasAnnounceSendEvidence() — recursive error-chain walker that detects send evidence from OutboundDeliveryError.sentBeforeError, generic sentBeforeError/visibleReplySent flags, and the promptError field of EmbeddedAttemptPromptErrorWithCleanupTakeoverError - Add isSessionFileChangedAnnounceError() helper - Safety valve in isTransientAnnounceDeliveryError(): session-file-changed errors without send evidence are treated as transient (retry allowed) - Safety valve in sendSubagentAnnounceDirectly catch block: permanent errors without send evidence fall through to fallback paths - 7 focused tests covering send-evidence detection and retry behavior Fixes openclaw#91527 Co-Authored-By: Claude <noreply@anthropic.com>
…nounce error Per ClawSweeper review feedback on openclaw#92274: the unconditional permanent-regex approach may prevent duplicates but can also suppress recovery for pre-send lock failures. This commit adds structured send-evidence checks so the session-file- changed takeover error is only treated as permanent when a visible send has already occurred. Changes: - Extract SESSION_FILE_CHANGED_ANNOUNCE_RE as a named constant - Add hasAnnounceSendEvidence() — recursive error-chain walker that detects send evidence from OutboundDeliveryError.sentBeforeError, generic sentBeforeError/visibleReplySent flags, and the promptError field of EmbeddedAttemptPromptErrorWithCleanupTakeoverError - Add isSessionFileChangedAnnounceError() helper - Safety valve in isTransientAnnounceDeliveryError(): session-file-changed errors without send evidence are treated as transient (retry allowed) - Safety valve in sendSubagentAnnounceDirectly catch block: permanent errors without send evidence fall through to fallback paths - 7 focused tests covering send-evidence detection and retry behavior Fixes openclaw#91527 Co-Authored-By: Claude <noreply@anthropic.com>
4791a82 to
d46a64b
Compare
…nounce error Per ClawSweeper review feedback on openclaw#92274: the unconditional permanent-regex approach may prevent duplicates but can also suppress recovery for pre-send lock failures. This commit adds structured send-evidence checks so the session-file- changed takeover error is only treated as permanent when a visible send has already occurred. Changes: - Extract SESSION_FILE_CHANGED_ANNOUNCE_RE as a named constant - Add hasAnnounceSendEvidence() — recursive error-chain walker that detects send evidence from OutboundDeliveryError.sentBeforeError, generic sentBeforeError/visibleReplySent flags, and the promptError field of EmbeddedAttemptPromptErrorWithCleanupTakeoverError - Add isSessionFileChangedAnnounceError() helper - Safety valve in isTransientAnnounceDeliveryError(): session-file-changed errors without send evidence are treated as transient (retry allowed) - Safety valve in sendSubagentAnnounceDirectly catch block: permanent errors without send evidence fall through to fallback paths - 7 focused tests covering send-evidence detection and retry behavior Fixes openclaw#91527 Co-Authored-By: Claude <noreply@anthropic.com>
|
@clawsweeper re-review |
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
… failure with send evidence When a required-mode batch send fails mid-batch after an earlier payload already succeeded, the wrapper catch in deliverOutboundPayloadsWithQueueCleanup called failDelivery. failDelivery only bumps retryCount/lastError; it does not advance recoveryState, so the entry stayed in send_attempt_started (set earlier by markDeliveryPlatformSendAttemptStarted via onPlatformSendStart). On the next Telegram reconnect, drainQueuedEntry sees send_attempt_started and calls reconcileUnknownQueuedDelivery. When adapter reconciliation misreports not_sent (the message was actually sent, per the outbound send ok / messageId evidence), the entry is replayed and the user receives a duplicate. Fix: when the error carries send evidence (OutboundDeliveryError with sentBeforeError === true and platformSendStarted === true), call markQueuedPlatformOutcomeUnknown instead of failDelivery. This advances the entry to unknown_after_send, which drain already routes through reconcileUnknownQueuedDelivery, preserving the entry for adapter reconciliation rather than leaving it in send_attempt_started for replay. When there is no send evidence (sentBeforeError === false), failDelivery remains correct: nothing reached the channel, so retrying is safe. This is a third duplicate path distinct from openclaw#89812 (mirror best-effort) and openclaw#92274 (subagent-announce-delivery retry); it is the outbound/deliver wrapper catch, which neither prior fix covers. Tests: - regression: two payloads, first succeeds, second throws; asserts markDeliveryPlatformOutcomeUnknown called, failDelivery/ackDelivery not. - guard: no send evidence; failDelivery still called.
… failure with send evidence When a required-mode batch send fails mid-batch after an earlier payload already succeeded, the wrapper catch in deliverOutboundPayloadsWithQueueCleanup called failDelivery. failDelivery only bumps retryCount/lastError; it does not advance recoveryState, so the entry stayed in send_attempt_started (set earlier by markDeliveryPlatformSendAttemptStarted via onPlatformSendStart). On the next Telegram reconnect, drainQueuedEntry sees send_attempt_started and calls reconcileUnknownQueuedDelivery. When adapter reconciliation misreports not_sent (the message was actually sent, per the outbound send ok / messageId evidence), the entry is replayed and the user receives a duplicate. Fix: when the error carries send evidence (OutboundDeliveryError with sentBeforeError === true and platformSendStarted === true), call markQueuedPlatformOutcomeUnknown instead of failDelivery. This advances the entry to unknown_after_send, which drain already routes through reconcileUnknownQueuedDelivery, preserving the entry for adapter reconciliation rather than leaving it in send_attempt_started for replay. When there is no send evidence (sentBeforeError === false), failDelivery remains correct: nothing reached the channel, so retrying is safe. This is a third duplicate path distinct from #89812 (mirror best-effort) and #92274 (subagent-announce-delivery retry); it is the outbound/deliver wrapper catch, which neither prior fix covers. Tests: - regression: two payloads, first succeeds, second throws; asserts markDeliveryPlatformOutcomeUnknown called, failDelivery/ackDelivery not. - guard: no send evidence; failDelivery still called.
… failure with send evidence When a required-mode batch send fails mid-batch after an earlier payload already succeeded, the wrapper catch in deliverOutboundPayloadsWithQueueCleanup called failDelivery. failDelivery only bumps retryCount/lastError; it does not advance recoveryState, so the entry stayed in send_attempt_started (set earlier by markDeliveryPlatformSendAttemptStarted via onPlatformSendStart). On the next Telegram reconnect, drainQueuedEntry sees send_attempt_started and calls reconcileUnknownQueuedDelivery. When adapter reconciliation misreports not_sent (the message was actually sent, per the outbound send ok / messageId evidence), the entry is replayed and the user receives a duplicate. Fix: when the error carries send evidence (OutboundDeliveryError with sentBeforeError === true and platformSendStarted === true), call markQueuedPlatformOutcomeUnknown instead of failDelivery. This advances the entry to unknown_after_send, which drain already routes through reconcileUnknownQueuedDelivery, preserving the entry for adapter reconciliation rather than leaving it in send_attempt_started for replay. When there is no send evidence (sentBeforeError === false), failDelivery remains correct: nothing reached the channel, so retrying is safe. This is a third duplicate path distinct from openclaw#89812 (mirror best-effort) and openclaw#92274 (subagent-announce-delivery retry); it is the outbound/deliver wrapper catch, which neither prior fix covers. Tests: - regression: two payloads, first succeeds, second throws; asserts markDeliveryPlatformOutcomeUnknown called, failDelivery/ackDelivery not. - guard: no send evidence; failDelivery still called.
… failure with send evidence When a required-mode batch send fails mid-batch after an earlier payload already succeeded, the wrapper catch in deliverOutboundPayloadsWithQueueCleanup called failDelivery. failDelivery only bumps retryCount/lastError; it does not advance recoveryState, so the entry stayed in send_attempt_started (set earlier by markDeliveryPlatformSendAttemptStarted via onPlatformSendStart). On the next Telegram reconnect, drainQueuedEntry sees send_attempt_started and calls reconcileUnknownQueuedDelivery. When adapter reconciliation misreports not_sent (the message was actually sent, per the outbound send ok / messageId evidence), the entry is replayed and the user receives a duplicate. Fix: when the error carries send evidence (OutboundDeliveryError with sentBeforeError === true and platformSendStarted === true), call markQueuedPlatformOutcomeUnknown instead of failDelivery. This advances the entry to unknown_after_send, which drain already routes through reconcileUnknownQueuedDelivery, preserving the entry for adapter reconciliation rather than leaving it in send_attempt_started for replay. When there is no send evidence (sentBeforeError === false), failDelivery remains correct: nothing reached the channel, so retrying is safe. This is a third duplicate path distinct from openclaw#89812 (mirror best-effort) and openclaw#92274 (subagent-announce-delivery retry); it is the outbound/deliver wrapper catch, which neither prior fix covers. Tests: - regression: two payloads, first succeeds, second throws; asserts markDeliveryPlatformOutcomeUnknown called, failDelivery/ackDelivery not. - guard: no send evidence; failDelivery still called.
…e failure The "session file changed while embedded prompt lock was released" error is a concurrent-session race: the parent transcript is being modified by turn-maintenance while the subagent completion tries to inject. Retrying does not help because each retry independently triggers a duplicate outbound send (3x duplicate Telegram messages reported in openclaw#91527). Add the error message pattern to PERMANENT_ANNOUNCE_DELIVERY_ERROR_PATTERNS so it stops triggering retries, mirroring the approach used in PR openclaw#89812 for the general outbound delivery path. Fixes openclaw#91527 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nounce error Per ClawSweeper review feedback on openclaw#92274: the unconditional permanent-regex approach may prevent duplicates but can also suppress recovery for pre-send lock failures. This commit adds structured send-evidence checks so the session-file- changed takeover error is only treated as permanent when a visible send has already occurred. Changes: - Extract SESSION_FILE_CHANGED_ANNOUNCE_RE as a named constant - Add hasAnnounceSendEvidence() — recursive error-chain walker that detects send evidence from OutboundDeliveryError.sentBeforeError, generic sentBeforeError/visibleReplySent flags, and the promptError field of EmbeddedAttemptPromptErrorWithCleanupTakeoverError - Add isSessionFileChangedAnnounceError() helper - Safety valve in isTransientAnnounceDeliveryError(): session-file-changed errors without send evidence are treated as transient (retry allowed) - Safety valve in sendSubagentAnnounceDirectly catch block: permanent errors without send evidence fall through to fallback paths - 7 focused tests covering send-evidence detection and retry behavior Fixes openclaw#91527 Co-Authored-By: Claude <noreply@anthropic.com>
…wave-20260617a (2)
f6e1edd to
8c055a8
Compare
|
Landed via squash onto main.
Thanks @fsdwen! |
… failure with send evidence When a required-mode batch send fails mid-batch after an earlier payload already succeeded, the wrapper catch in deliverOutboundPayloadsWithQueueCleanup called failDelivery. failDelivery only bumps retryCount/lastError; it does not advance recoveryState, so the entry stayed in send_attempt_started (set earlier by markDeliveryPlatformSendAttemptStarted via onPlatformSendStart). On the next Telegram reconnect, drainQueuedEntry sees send_attempt_started and calls reconcileUnknownQueuedDelivery. When adapter reconciliation misreports not_sent (the message was actually sent, per the outbound send ok / messageId evidence), the entry is replayed and the user receives a duplicate. Fix: when the error carries send evidence (OutboundDeliveryError with sentBeforeError === true and platformSendStarted === true), call markQueuedPlatformOutcomeUnknown instead of failDelivery. This advances the entry to unknown_after_send, which drain already routes through reconcileUnknownQueuedDelivery, preserving the entry for adapter reconciliation rather than leaving it in send_attempt_started for replay. When there is no send evidence (sentBeforeError === false), failDelivery remains correct: nothing reached the channel, so retrying is safe. This is a third duplicate path distinct from openclaw#89812 (mirror best-effort) and openclaw#92274 (subagent-announce-delivery retry); it is the outbound/deliver wrapper catch, which neither prior fix covers. Tests: - regression: two payloads, first succeeds, second throws; asserts markDeliveryPlatformOutcomeUnknown called, failDelivery/ackDelivery not. - guard: no send evidence; failDelivery still called. (cherry picked from commit 71422a9)
What Problem This Solves
Subagent completion announcements can retry after an embedded prompt lock-change race even when the first attempt already produced a visible outbound send. On Telegram this can show the same completion message multiple times.
Fixes #91527.
Why This Change Was Made
The announce path now treats session-file-changed failures as retryable only when there is no send evidence. If the error chain contains committed send evidence, the failure is terminal for that announce attempt, so dispatch does not fallback-steer or retry into a duplicate send.
The cleanup pass also carries terminal delivery state through subagent lifecycle cleanup so completed runs are finalized instead of scheduling another completion retry.
User Impact
Users should see one subagent completion message for this prompt-lock race instead of repeated duplicate completion messages.
Evidence
node scripts/run-vitest.mjs src/agents/subagent-announce-delivery.test.ts src/agents/subagent-announce.test.ts src/agents/subagent-registry-lifecycle.test.ts.agents/skills/autoreview/scripts/autoreview --mode localgit diff --checkProof gap: no live Telegram transcript or recording was added in this branch. The fix is covered by focused source-level regression tests for duplicate-send prevention, retry-without-evidence, and lifecycle cleanup finalization.