A restarted agent can correctly recover its work and still send the same message twice.
That happens when execution recovery and outbound-delivery recovery share a vague notion of “done.” A session transcript may show that a tool call finished, while the delivery path still has a pending outbound item. After a restart, both the old pending item and a newly reconstructed item can be sent.
The fix is to model these as two separate state machines.
Execution recovery is not delivery recovery
Execution recovery answers: did the agent finish the unit of work?
Outbound-delivery recovery answers: did the external system accept the notification, webhook, email, or chat message?
They have different failure boundaries. An agent can complete a task and crash before recording the result. A delivery worker can submit a message and crash before acknowledging it. Treating both as one boolean creates duplicate work or missing notifications.
A minimal execution record can track:
- run_id
- step_id
- input_hash
- execution_status
- result_ref
- started_at and finished_at
A separate delivery record should track:
- delivery_id
- run_id and result_ref
- destination and payload_hash
- delivery_status
- provider_message_id
- attempt_count
- next_attempt_at
- acknowledged_at
The delivery record needs its own durable identity. Do not recreate it from the latest conversation transcript on startup.
Use a durable delivery queue
The reliable sequence is:
- Commit the execution result.
- Commit an outbound delivery item that references that result.
- Let a delivery worker claim the item with a lease.
- Submit it using a stable idempotency key when the provider supports one.
- Persist the provider response or an explicit unknown state.
- Acknowledge and remove the item atomically with the delivery ledger update.
The hard case is step 5. If the provider accepted the message and the worker crashed before the acknowledgement, the next worker must not blindly send a new message. It should query by idempotency key or provider message ID. If reconciliation is impossible, surface an operator-review state instead of pretending the send failed.
This is the same control-plane lesson as an agent run ledger: the record must preserve enough evidence to explain what happened after a crash. My earlier article, Agent observability starts with a run ledger, focuses on execution evidence. Here, the delivery ledger is the missing second half.
Test the restart boundaries directly
Do not test only “restart the agent and continue.” Inject a crash at each boundary:
- Before the execution result is committed.
- After the result is committed but before the delivery item exists.
- After the delivery item exists but before the worker claims it.
- After provider acceptance but before the local acknowledgement.
- After the acknowledgement but before queue cleanup.
- While two workers claim the same item.
For each test, assert these invariants:
- A completed run has exactly one durable delivery identity per intended destination.
- Restart does not manufacture a second delivery item.
- A retry reuses the same idempotency identity.
- An accepted message is either reconciled or marked unknown.
- Queue cleanup cannot erase the only audit record.
- The operator can distinguish pending, delivered, failed, and unknown.
A useful failure-injection harness records the boundary name before terminating the worker, then restarts from a clean process with the same database. Count provider-side effects, not just local logs. A green local test that never checks the recipient is not a duplicate-send test.
Hosting changes the recovery story
A long-running agent needs durable shared state, not just a process supervisor. A service manager can restart a crashed process, but it cannot reconstruct a safe delivery decision from volatile memory. Store the execution and delivery ledgers on persistent storage, back them up, and include them in restore tests.
If you run the agent on managed OpenClaw hosting such as always-on OpenClaw hosting on Ampere, keep the product claim narrow: hosting can provide a place for the runtime to stay available, but your application still needs explicit delivery state, idempotency, reconciliation, and duplicate-send tests. Availability is not the same thing as delivery correctness.
The operational checklist
Before calling an agent restart-safe, ask:
- Is execution state durable and versioned?
- Is outbound delivery represented by a separate durable queue?
- Does every delivery have a stable identity?
- Can the provider be queried after a timeout?
- Are leases and acknowledgements crash-safe?
- Are credentials and destination scopes visible in the audit trail?
- Can a restore reproduce the decision without replaying the side effect?
- Is “unknown” an explicit state that blocks unsafe retries?
The key question is not “did the process come back?” It is:
After a crash, can I prove which work completed and which external side effects were accepted?
If those answers come from different ledgers, recovery becomes testable. If they come from one transcript, a restart can turn uncertainty into a duplicate message.
답글 남기기