Problem
pollRun acks a run's recovery delivery as soon as the run reaches a terminal state (src/slack/core-bridge.ts:154-156), which tombstones the durable copy. The reply itself is posted afterwards by the turn handler (src/slack/turn-handler.ts:543,548) through postReply, which is a plain chat.postMessage with retries disabled (NO_RETRY, src/slack/config.ts:3, applied at src/slack/index.ts:47) and carries no idempotency metadata.
If that single post attempt fails — a chat.postMessage rate limit, a transient 5xx, or a redeploy during the attachment upload window — the exception is swallowed, the run is already terminal, and the delivery is already stamped delivered. The user never receives the answer and nothing retries it.
The quarantine refusal path deliberately does the opposite, posting before it acks (postThenAckRunDelivery, src/slack/refusals.ts:15-27).
The current ordering is load-bearing for duplicate suppression: the live post carries no qm_delivery metadata, so the delivery poller's verify-first scan could not recognize it and would repost. Any fix needs to preserve that.
Expected behavior
A run's reply is durably recoverable until it has actually been posted. A post failure leaves the delivery pending so the existing poller retries it, without introducing a duplicate for the common case where the live post succeeded.
Acceptance criteria
- The recovery delivery is acked only after the reply has been posted successfully.
- The live post carries the same identifying metadata the delivery poller verifies against, so a retry cannot double-post.
- A failed live post leaves the delivery claimable, and the poller delivers it.
- Tests cover a failing
chat.postMessage and assert the user still receives exactly one reply.
Problem
pollRunacks a run's recovery delivery as soon as the run reaches a terminal state (src/slack/core-bridge.ts:154-156), which tombstones the durable copy. The reply itself is posted afterwards by the turn handler (src/slack/turn-handler.ts:543,548) throughpostReply, which is a plainchat.postMessagewith retries disabled (NO_RETRY,src/slack/config.ts:3, applied atsrc/slack/index.ts:47) and carries no idempotency metadata.If that single post attempt fails — a
chat.postMessagerate limit, a transient 5xx, or a redeploy during the attachment upload window — the exception is swallowed, the run is already terminal, and the delivery is already stamped delivered. The user never receives the answer and nothing retries it.The quarantine refusal path deliberately does the opposite, posting before it acks (
postThenAckRunDelivery,src/slack/refusals.ts:15-27).The current ordering is load-bearing for duplicate suppression: the live post carries no
qm_deliverymetadata, so the delivery poller's verify-first scan could not recognize it and would repost. Any fix needs to preserve that.Expected behavior
A run's reply is durably recoverable until it has actually been posted. A post failure leaves the delivery pending so the existing poller retries it, without introducing a duplicate for the common case where the live post succeeded.
Acceptance criteria
chat.postMessageand assert the user still receives exactly one reply.