DEV Community

wzg0911
wzg0911

Posted on

The "Proof-of-State" Trap: Why Your Agent Dies After Every Restart

I've spent the last few weeks doing autopsies on a string of agent crashes — not models hallucinating, but plain old infrastructure lying to itself after a restart. They looked unrelated. They weren't.

They share one root cause. I'm calling it the Proof-of-State Trap: any judgment of "X is still alive / still yours / still the current instance" that relies on an identifier which becomes invalid after a restart, silently turns into a permanent veto.

Here are the four cases that convinced me.

Case 1 — Session handle retired, new one never handed back (#113434)

An upstream generation was retired; the server had already rotated to a new handle, but the client was still holding the old one. Symptom: mysterious 400s / "session invalid." Root cause: state lifecycle wasn't aligned across the restart. This diagnosis was later independently confirmed by merged upstream PRs (#114056 / #114401 / #114478).

Case 2 — PID lock fooled by container reuse (#114234)

A "is the lock held?" check used PID liveness. After a container rebuild, the PID got reused, so the lock looked "perpetually alive" and the cache froze. Root cause: the proof of lock validity was an identifier that expires on restart.

Case 3 — Gateway restart leaves session running (#114255)

After a gateway restart, a session's status string stayed stuck at running, so downstream code concluded "it's still going" and blocked forever. Root cause: a pre-restart state was treated as current liveness.

Case 4 — LaunchAgent one-shot update becomes a permanent restart loop (#115326)

This is the one that made the pattern undeniable. A community developer (robingutsche) traced his "midnight crash, restart fixes it, crashes again" loop to a launchctl submit -l com.openclaw.beta-update-once job that had keepalive set — so when the one-shot update failed with EACCES, launchd restarted it forever, repeatedly killing the managed gateway and tripping the breaker. He also found the breaker never auto-clears, and status --all reports OK even when the channel is actually not-running.

He wrote it up as a maintainer-grade reproduction. I organized it into three independent bugs:

  • A (update orchestration): a one-shot update job must not carry keepalive; on failure it must clean up its launchd job.
  • B (breaker never drains): same family as #114255 — a status written before a restart is read as current liveness.
  • C (status misreport): valid config ≠ healthy runtime (same family as #99725, which loaded the wrong kind of proof into a field).

The invariant

All four collapse into one actionable rule:

Proof-of-state must be bound to an incarnation. Any "X is still alive / still belongs to you / is still the current instance" check that leans on a restart-fragile identifier — a PID, a stale handle, a pre-restart status string, an old breaker latch — is a time bomb.

This is also why I built ARK (Agent Reliability Kit): to make "does this state claim match the current instance?" an automatically checkable health item, instead of something you discover at 3am.

Credits

The #115326 root cause is entirely robingutsche's — I only organized it. If you're fighting an agent that crashes at night, restarts, and crashes again, drop your logs in the free diagnostic thread. I won't hand you an auto-generated fake report; I'll read it with you, first-person, like the post-mortem above.

Top comments (0)