Message delivery
Message delivery
Delivery guarantee: at-least-once
RemoteClaw delivers outbound messages at least once.
It does not provide exactly-once delivery, and it cannot. The acknowledgement that a message reached a third-party chat platform travels back over the same unreliable link the message went out on, so there is no bookkeeping on the gateway side that makes “the platform accepted it” and “we recorded that it accepted it” one atomic fact. That is the Two Generals problem — it is a property of talking to a system we do not control, not a gap a better queue closes.
Practical consequences:
-
A message can be delivered more than once. Assume it, and design around it.
-
Idempotency and de-duplication belong to the consumer and the platform. RemoteClaw does not attach an idempotency key to outbound sends, so nothing downstream can collapse a duplicate on your behalf. If a workflow reacts to agent output, make the reaction idempotent.
-
The duplicate window is bounded and surfaced, not eliminated. The gateway narrows it in two places and makes what survives visible rather than silent:
- a per-entry in-flight marker means a process that dies mid-send does not blind-replay on the next start;
- a send that fails ambiguously — a timeout, a reset, a 5xx after the request reached the transport — is not retried either, because “we got no result” is not “nothing arrived”.
In both cases the entry is moved to a quarantine directory for a human instead of being re-sent. Failures that provably did not reach the recipient (the connection was refused, the name did not resolve, the platform rejected the chat outright, or the send never started) still retry normally.
This applies to both senders — the live send, and the retry the recovery pass makes on its own at startup — and to both ways a send can fail: raising an error, or, under
bestEffort, reporting each payload’s failure and returning. All four combinations classify with the same predicate, so an ambiguous failure is quarantined whichever one hit it. A recovery retry that times out after reaching the wire is held for review rather than replayed on the next restart, and so is abestEffortrecovery retry whose payloads failed that way with nothing landed.One residual window remains: the in-flight marker is written with an atomic rename but is not flushed to stable storage, so a power loss can take the marker with it and a post-reboot recovery pass has nothing left to refuse.
The needs-review reconciliation queue
Entries whose delivery outcome cannot be determined are moved to:
$REMOTECLAW_STATE_DIR/delivery-queue/needs-review/<id>.jsonThey are never retried automatically. Every recovery pass — that is, every gateway start — logs the standing backlog, so quarantined mail stays visible long after the start that quarantined it.
To reconcile one:
- Open the entry and read
to,payloads, andplatformSendStartedAt. - Check the recipient’s message history around that time.
deliveredBeforeFailure, when present, tells you how many message parts were confirmed sent before the failure — the rest are the ones in question. When it is absent, nothing was confirmed either way. - If the message arrived, delete the file.
- If it did not and you want it sent, move the file back into
delivery-queue/, set itsrecoveryStatetonulland itsretryCountto0, then restart the gateway. Recovery only runs at startup; leavingrecoveryStateset only re-quarantines the entry, and leavingretryCountat its current value can file it underfailed/without sending.
Prune reconciled entries. Nothing expires them for you, and § Backups below is the reason that matters.
Backups contain undelivered message content
The rest of the delivery queue is filtered out of backups as volatile: it churns
during a live backup and has no restoration value. needs-review/ is
deliberately carved out of that filter, because it is the opposite kind of data —
terminal, unique, and the operator’s only record that a message’s outcome is
undetermined. Dropping it on restore would silently erase a to-do list.
That carve-out has a cost worth stating plainly: backups travel further than
live state. They go off-box, to object storage or another operator’s laptop,
and they are retained long after the entry they contain was reconciled. Live
state sits in a 0700 directory on one host under one trust boundary; a backup
does not.
So:
- Hold backups of the state directory to the same standard as message transcripts, not to the standard of configuration. Encrypt them at rest, and keep the retention window as short as your recovery objective allows.
- Reconcile and prune
needs-review/on a schedule. An entry deleted before the next backup never enters the backup chain at all — this is the cheapest control available, and it is the same hygiene the queue already asks for. - If you need to share a backup for support or diagnostics, remove
delivery-queue/needs-review/first. Nothing in it is needed to reproduce a configuration problem.
Related: Security for the on-disk trust boundary and filesystem permissions, and Logging for redaction and retention of the log surfaces that reference these entries.