[Fix] Stop closeout retry loops when chat delivery permanently fails - #1095
Conversation
An automation task whose Slack channel rejects every chat.postMessage got stuck in a loop: the stop hook demands a terminal closeout, the closeout post fails with an opaque generic 502, the agent retries, and the hook re-blocks on the next idle. The task burned reminder budget on 15 failed posts and then completed silently with no closeout delivered anywhere. Two fixes: - Propagate the real Slack error. SlackNotifier.postMessageDetailed preserves the structured Slack error code (not_in_channel, invalid_auth, ...) instead of collapsing every failure into a missing timestamp, and the thread_reply MCP handler returns it with a structured retryable verdict (422 for permanent errors, 502 for transient ones). - Give the closeout requirement an escape hatch. The worker records failed deliveries in the reply-satisfaction state; a non-retryable error (or five failed attempts) stamps a terminal delivery failure, the send_chat_reply result tells the agent to stop retrying, and the stop/silence hooks stand down so the task can complete instead of being reminded into a post that cannot succeed. A later successful post or a new inbound turn clears the stamp.
|
No new code issues found. See task
Reviewed 796b095 |
A successful reaction goes through reactions.add, not chat.postMessage, so it proves nothing about the posting path. Clearing the delivery failure state on reactions would re-arm closeout enforcement against a channel that still rejects posts.
|
@roomote-roomote Addressed in 796b095 / responded below:
|
Problem
An automation-started task whose Slack channel rejects every
chat.postMessagegets wedged in a closeout retry loop:automation_missing_terminal_closeout) and reminds the agent to post withsend_chat_replypurposecloseout.SlackNotifier.sendMessagelogs it server-side and returns nothing, and thethread_replyMCP handler collapses it into a generic502 Slack chat.postMessage returned no message timestamp.In a real occurrence the agent made 15 failed post attempts over ~6 minutes before the harness reminder cap fired, and the run then completed silently with the deliverable never posted anywhere. The closeout requirement had no escape path: satisfaction is only recordable from a successful post, which was impossible.
Fix
Propagate the real Slack error (API side)
SlackNotifier.postMessageDetailedreturns a structured outcome:tson success, otherwise the Slack error code, a transport-error flag, or the skipped-missing-thread-root marker.postMessagekeeps its existing signature and delegates.thread_replyMCP handler throws a typedSlackPostDeliveryErrorand maps it to a structured response:{ error, slackErrorCode, retryable }, status 422 for permanent errors (not_in_channel,channel_not_found,is_archived,invalid_auth,token_revoked, ...) and 502 for transient ones. Content-dependent errors (msg_too_long,invalid_blocks) stay retryable since a rewritten message can succeed.Escape hatch (worker side)
retryable: falseresponse into a typedChatDeliveryErrorcarrying the provider error code (structured fields, no message-wording matching).send_chat_replyfailures from the delivery call are recorded in the reply-satisfaction state file: a non-retryable error, or 5 failed attempts (bounded fallback for unclassified errors), stampsterminalDeliveryFailureAtMs.terminal_delivery_failureallow) instead of demanding an undeliverable closeout, so the task completes cleanly with the outcome in the transcript.Not in this PR
send_chat_replyon every provider (Slack, Teams, Telegram, Discord) via the bounded failure budget, since it instruments the shared tool path. The fast path (immediate terminal on a structured non-retryable code) is Slack-only for now; surfacing structured error codes from the other providers is a follow-up.post_to_channelkeeps its generic errors and deliberately does not feed the delivery-failure budget: it can target arbitrary channels, so its failures must not poison the bound thread's closeout state. Structured error codes for it are part of the same follow-up.Validation
pnpm lint,pnpm check-types,pnpm knipall pass.@roomote/slacksuite (350 tests), full@roomote/workersuite (1619 tests; the 4 failures intail-file-path/command-executorare pre-existing on develop in this environment), andapps/apiMCP handler suite (165 tests) pass.postMessageDetailedoutcomes, error-code classification, the 422/502/409 handler mapping, delivery-failure recording (count, terminal stamp, clear-on-success, reset-on-new-turn, non-parent-session guard), the terminal rewrite of the tool result, andterminal_delivery_failureallows in both hook scripts.