Browser automation agents rarely fail because the model cannot click a button. They fail because the browser, worker, and external side effect disagree about what happened.
A worker can die after submitting a form but before recording success. A browser can stay open while its session expires. A retry can then create a duplicate ticket, purchase, or deployment. “Run it again” is not a recovery strategy.
This post shows a small control plane for restart-safe browser tasks. The same shape works for OpenClaw jobs and other long-running agent workflows.
1. Give every run a lease
A run needs an identity that survives model calls and process restarts:
task_id = the business operation
run_id = this attempt
lease_id = the worker's current ownership
step_id = the idempotent unit of work
Store the lease with an expiry and renew it from a supervisor, not from the model loop. If the lease expires, a second worker may reconcile the run, but it must not immediately replay the last side effect.
A minimal record can look like this:
desired_state: RUNNING
observed_state: UNKNOWN
run_id: 8f...
step_id: submit-invoice-04
lease_expires_at: 2026-08-07T12:00:00Z
last_checkpoint: form-filled
Keep desired and observed state separate. “The browser process exists” is not evidence that the business step completed.
2. Checkpoint before and after side effects
Use checkpoints that describe externally verifiable facts, not vague progress messages:
- page_loaded
- form_filled
- submit_intent_recorded
- submit_dispatched
- result_reconciled
Persist submit_intent_recorded before clicking. Persist submit_dispatched immediately after the click attempt. If the worker dies between those writes, recovery must classify the step as UNKNOWN and reconcile it using a request key, confirmation page, API lookup, or human approval.
Do not turn UNKNOWN into FAILED just because the browser disconnected. Retrying an unknown mutation is how duplicate side effects happen.
3. Prefer a request key over UI memory
If the destination supports idempotency keys, derive one from the business operation:
def request_key(task_id, step_id):
return sha256(f"{task_id}:{step_id}".encode()).hexdigest()
Put that key in the API request or a stable form field when the system supports it. For UI-only systems, keep a local outbox containing the key, normalized target, and payload hash. Recovery can then compare the target's state before deciding whether another click is safe.
A screenshot is useful evidence, but it is not an idempotency mechanism.
4. Reconcile after restart
On startup, run reconciliation before asking the model for the next action:
def reconcile(step):
if step.state == "SUCCEEDED":
return "CONTINUE"
if step.state == "DISPATCHED":
return lookup_by_request_key(step.request_key)
if step.state == "UNKNOWN":
return "REQUIRE_RECONCILIATION"
if lease_expired(step):
return "ACQUIRE_LEASE_THEN_RECHECK"
return "RESUME"
The important property is that recovery is deterministic. The model may help interpret an unfamiliar confirmation page, but it should not decide that a possibly completed payment, email, or deployment is safe to repeat without a policy check.
5. Test the failure window, not just the happy path
A useful fixture matrix injects termination at each boundary:
| Failure point | Expected recovery |
|---|---|
| before intent record | safe to resume |
| after intent, before dispatch | re-check lease and target |
| after dispatch, before response | UNKNOWN, reconcile |
| after response, before checkpoint | reconcile from request key |
| browser crash during navigation | preserve step state, reacquire session |
| lease expiry with live browser | fence the old worker |
| duplicate delivery | same result, no second side effect |
Log at least task_id, run_id, step_id, lease ID, request key, target fingerprint, policy version, and the final SUCCEEDED, FAILED, NOT_SENT, or UNKNOWN outcome. Redact cookies, tokens, and page contents that may contain secrets.
6. Keep the browser disposable
The browser profile should be treated as a replaceable execution surface. Store durable state outside it, scope credentials to the smallest useful action, and verify that a terminated worker cannot keep sending commands.
For an always-on OpenClaw or browser-agent controller, managed hosting such as always-on OpenClaw hosting on Ampere can solve the runtime-placement problem. It does not solve idempotency, authorization, or recovery logic; those controls still belong in the application.
The practical rule
Before adding another autonomous browser action, answer three questions:
- What durable record proves the intent and the outcome?
- How does recovery distinguish NOT_SENT from UNKNOWN?
- What prevents an expired worker from replaying the side effect?
If those answers are not testable, the agent is not restart-safe yet. Start with one mutation, inject crashes around it, and make reconciliation boring and deterministic before expanding the tool surface.
Follow if you build AI agents and developer tools where runtime behavior matters as much as model capability.
Top comments (0)