A backup is not a recovery plan until a fresh worker can restore it and continue without guessing.
For OpenClaw, coding agents, and browser automations, the dangerous failure is often not data loss. It is partial recovery: the files return, but the agent has stale credentials, an expired browser session, missing schedules, an unreadable vector index, or a memory snapshot that points at work the external system never received.
This post turns backup verification into a small, repeatable restore drill.
Define what “restored” means
Start with a recovery contract instead of a list of directories. A useful contract answers four questions:
- Can a new worker boot from the backup without the old machine?
- Can it identify the last durable task state?
- Can it distinguish work that was not sent from work whose result is unknown?
- Can an operator see what was restored, what was rejected, and what needs approval?
The fourth question matters. A restore command that silently skips an unreadable file creates a plausible but incomplete agent.
Write the contract down as assertions:
after_restore:
workspace_readable: true
config_schema_valid: true
credential_scope_verified: true
scheduler_reconciled: true
pending_side_effects_classified: true
audit_log_appendable: true
operator_approval_required_for_unknown: true
~~~
These are control-plane checks. They are more useful than “the process started.”
## Separate durable state from disposable state
Do not treat the whole home directory or browser profile as one backup unit. Classify each item:
| State | Examples | Restore rule |
| --- | --- | --- |
| Durable | task journal, prompts, approved plans, request keys | restore and validate schema |
| Reconstructable | caches, embeddings, package downloads | rebuild and record the source version |
| Secret | API keys, cookies, refresh tokens | restore only through a scoped secret path |
| Disposable | browser profile, PID files, sockets | do not restore blindly |
| External | sent email, deployed code, created ticket | reconcile against the destination |
A browser profile can contain useful session state, but restoring it blindly can also resurrect an expired cookie or a profile with more privileges than the new runtime should have. Treat the browser as replaceable and keep the evidence needed to reconcile its work outside the browser.
For the durable part, use an explicit manifest. For example:
~~~text
state/task-journal.sqlite sha256:...
state/outbox.jsonl sha256:...
state/schedules.json sha256:...
config/agent.schema.json sha256:...
meta/runtime-version.txt sha256:...
~~~
The manifest should include the backup timestamp, source runtime version, schema version, and whether each item was encrypted. Never put raw tokens in the manifest.
## Make the outbox the recovery boundary
Every operation that can create an external side effect should have a durable outbox record before dispatch:
~~~json
{
"operation_id": "issue-184:comment-02",
"request_key": "sha256(issue-184:comment-02)",
"target_fingerprint": "repo=example/project;issue=184",
"payload_hash": "sha256(...) ",
"state": "DISPATCHED",
"sent_at": "2026-08-08T01:20:00Z",
"result": "UNKNOWN"
}
~~~
On restore, do not let the model decide what to retry. Reconcile records with state DISPATCHED or UNKNOWN using the destination's lookup API, an idempotency key, or an explicit operator decision.
A safe result vocabulary is:
- NOT_SENT: no dispatch was recorded and the destination was not changed.
- SUCCEEDED: the destination confirms the requested effect.
- FAILED: the destination confirms that it did not apply the effect.
- UNKNOWN: dispatch may have happened; reconciliation is required.
Never convert UNKNOWN into FAILED merely because the old worker disappeared.
## The five-minute restore drill
Run this from a clean directory or disposable machine, not on the production worker:
~~~bash
set -euo pipefail
RESTORE_DIR="$(mktemp -d)"
./agent-backup verify --input backup.tar.zst --manifest manifest.json
./agent-backup restore --input backup.tar.zst --output "$RESTORE_DIR"
./agent validate-config --root "$RESTORE_DIR"
./agent migrate-state --root "$RESTORE_DIR" --dry-run
./agent reconcile-schedules --root "$RESTORE_DIR" --mode report
./agent reconcile-outbox --root "$RESTORE_DIR" --mode report
./agent smoke-test --root "$RESTORE_DIR" --no-external-writes
~~~
The important flag is **no external writes**. The first boot after restore should be read-only. It should prove that configuration, state, and tool schemas load before it is allowed to send an email, modify a repository, create a ticket, or launch a deployment.
Record at least:
- backup age and manifest verification result
- restore duration
- restored and rejected item counts
- schema migrations applied
- schedules that were disabled or duplicated
- outbox records in each result state
- credentials loaded and their scopes, never their values
- runtime and model versions
That gives you an actual recovery point objective and recovery time objective for the agent, rather than a guess.
## Inject the failures that backups hide
A restore drill is incomplete if it only tests a clean archive. Add fixtures for:
1. a truncated archive
2. one file with a checksum mismatch
3. an older state schema
4. a missing secret reference
5. a duplicate scheduled job
6. an outbox item stuck at UNKNOWN
7. a task journal whose last write was interrupted
8. a restored browser session with an expired cookie
9. a runtime version that cannot load a stored tool schema
For each fixture, define whether recovery should stop, quarantine the item, rebuild it, or require approval. “Best effort” is not a policy.
You can test the journal boundary with a forced termination between the outbox write and the network call, then restore and assert that the result is NOT_SENT or UNKNOWN based on the evidence available. If the test cannot tell those cases apart, the production system cannot either.
## Where a managed runtime fits
A managed runtime can remove one class of failure: an agent that only runs while a laptop is awake. If you choose managed hosting, evaluate persistent storage, exportable state, scoped credentials, scheduled backups, restore tests, outbox reconciliation, and a way to inspect failed recovery. Runtime placement does not solve backup correctness.
## A restore is successful only when the next action is safe
The final assertion should not be “the agent answered a prompt.” It should be:
> After restore, the worker can explain its durable state, classify every pending side effect, pass a read-only smoke test, and refuse ambiguous mutations until reconciliation is complete.
If you build AI agents or developer tools, follow for practical notes on runtime behavior, recovery, testing, and the control plane around model calls. A backup that has never been restored is a hope with a timestamp.
Top comments (0)