Truthful send states, hard channel contract, and outbox safeguards - #166
Open
MaxGhenis wants to merge 1 commit into
Open
Truthful send states, hard channel contract, and outbox safeguards#166MaxGhenis wants to merge 1 commit into
MaxGhenis wants to merge 1 commit into
Conversation
After the 2026-08-05 incident — a send reported ok/settled/confirmed sat ~15 hours before transmitting, a manual retry double-texted the recipient, and WhatsApp sends 404ed while status said connected: - transport_state queued/transmitted/delivered/uncertain/failed/canceled in every durable send result; settled and transmitted are true only on transport acknowledgment; results carry the platform actually used and the conversation_id written to; "delivery confirmed" wording removed - per-platform send capability (new internal/sendcap) published at /api/status "send", rendered by get_status in both serve modes, read by resolve_contact_routes, and enforced at send time: hard-down platforms (unpaired, adapter unregistered, auth revoked) refuse without queuing; transient disconnects queue with truthful reporting - outbox send window (migration 0011, expires_at_ms): a send still queued when its window closes cancels as expired instead of transmitting stale; MCP sends default to 10 minutes (ttl_seconds, OPENMESSAGES_SEND_TTL_SECONDS); the lease query excludes expired rows so a race can never transmit one; cutover carries windows forward - near-duplicate guard: a text >= 0.75-similar (normalized Levenshtein) to one submitted to the same conversation within 10 minutes is refused, naming the prior intent, unless force=true; same-key replays keep idempotent dedup; SendAgain untouched - list_outbox / cancel_outbox MCP tools in both serve modes - wait_for_transmit + wait_seconds hold the call through auto-retrying states until transport acknowledgment - explanatory 404s (conversation unresolvable in the serving store), optional platform assertion on send_to_conversation, and an explicit imessage cannot-send refusal Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rebuilds the send pipeline's reporting and safeguards after the 2026-08-05→06 incident:
send_messagereturned{ok:true, settled:true, state:"confirmed"}for a message that did not reach the recipient until ~15 hours later — seconds behind a manual day-of retry, double-texting the recipient — whilesend_to_conversationon awhatsapp:*JID returned a bare HTTP 404 even thoughget_statusshowedwhatsapp connected:trueandv2_send:true, andresolve_contact_routessaidsendable:false. Three surfaces, three answers, and a success receipt that was a claim about the local ledger, not the world.1. Truthful send states
Every durable send result now reports
transport_state:queuedqueued/dispatching/not_dispatched)transmitteddelivereduncertainsettled:false+uncertain:true(previously counted as settled)failed/canceledsettledandtransmittedare true only on transport acknowledgment (confirmed/store_failed). The "Message delivery confirmed" wording is gone everywhere (MCP text, CLI); transmitted results carry explicit "transport acceptance is not delivery" guidance. Results include theplatformactually used (sms/whatsapp/signal— RCS is not distinguishable at this layer and is deliberately not guessed) and theconversation_idwritten to. The daemon's v1 delivery responses carryaccount_id/conversation_id/platform/expires_at_ms/expiredso the MCP client can relay them.2. Hard channel contract
queueable) still queues, truthfully reported and bounded by the TTL.send_to_conversation/send_media_to_conversationgain an optionalplatformassertion that fails on mismatch (platform_mismatch) instead of sending on an unintended channel.platform: "imessage"gets an explicit "import/read-only, cannot send" refusal.HTTP 404: not found.3. Status honesty
/api/statuspublishes asendblock —send.{sms,whatsapp,signal}withavailable/queueable/reason— computed in the newinternal/sendcappackage from transport snapshots (paired, connected,needs_repair,auth_expired,phone_responding,needs_reauth) plus the v2 adapter registry (TextSend).get_statusrenders it in both serve modes and its text now states thatconnected/v2_sendalone never imply a platform can send.resolve_contact_routessendability,get_status, and send-time enforcement all read the same source (daemon truth in client mode — previously the MCP client judged sendability from its own transportless, always-disconnected process state), and routes carry asendable_reason.4. Outbox safeguards
expires_at_mson the outbox (migration 0011): an intent still queued when its window closes is canceled (error_class:"ttl", surfaced asexpired:true/ "NOT SENT") instead of transmitting stale. The lease query independently excludes expired rows, so a sweep/dispatch race can never transmit one. MCP sends default to 10 minutes (ttl_secondsper call,OPENMESSAGES_SEND_TTL_SECONDSper install,0= never expire); app/UI sends are unchanged (no TTL unless requested). Scheduled sends measure the window from theirnot_beforetime.near_duplicate_blocked) naming the prior outbox item — unlessforce:true. Same-key replays (the documented lost-response retry) bypass the guard and hit idempotent dedup, andSendAgain(an explicit user action) is untouched.list_outbox/cancel_outboxMCP tools (both serve modes): see everything still queued/retrying/uncertain, and stop a send before it crosses the transport boundary. During the incident there was no way to see or stop the overnight-queued message from the MCP surface.5. MCP ergonomics
wait_for_transmit: trueholds the call through auto-retrying states until transport acknowledgment or terminal failure, bounded bywait_seconds(default 25, max 120), so an agent can report truthfully in one call. Interrupted waits re-read the durable state on a detached context and keep the do-not-resend guidance.Tests
83 new/updated assertions across the stack, including the three called out in the spec:
TestDaemonQueuedSendNeverReportsSettledOrConfirmed(fake daemon stuck inqueued; assertssettled:false,ok:false,transmitted:false, no "delivery confirmed" text) plusTestSendPayloadSettledMatrixpinning settled/transmitted/transport_state for all 8 outbox states.TestSendToConversationPlatformAssertionMismatch, plus send-time enforcement (TestDaemonSendBlockedWhenPlatformCannotSend: hard-down refuses without submitting; queueable outage still queues).TestExpiredQueuedSendIsCanceledAndNeverDispatched)./api/statussend block, localapi wire round-trips, migration-gate update (staged-store pin now schema 11 + 0011 checksum).go test ./...green locally (including the R5 migration integration suite);gofmtclean on all touched files.Notes for review
settledsemantics changed foruncertain(wastrue, nowfalse+uncertain:true): an unknown outcome is not a settled one; reporting it settled is what invited "treat as done" during the incident.not_dispatchedkeepssettled:false/auto_retry:true.internal/tools/send_result.go).sendblock are treated as unknown capability (never blocked on), and their missing delivery fields degrade gracefully.🤖 Generated with Claude Code