DEV Community

Discussion on: A counter in process memory is not a guard: 131 restarts proved it

Collapse
 
vinhnguyenthanhdn profile image
Vinh Nguyen

One thing in your own description pulls against itself, and it's the half that decides whether the marker survives your 131 restarts. You describe it as touched at the top of each round, but the question you want it to answer is how long since a completed round. Those are the same file only when rounds finish. A crash loop that gets far enough to touch the file and then dies refreshes it 131 times and the stale alarm never fires — the marker ages only if the restart lands before the touch, which is a race, not a design.

Moving the write to the bottom of the round fixes it and costs you the other direction: a round that hangs forever after the touch would have been invisible under the top-of-round version too, so you're not losing anything you had. What you gain is that the amnesia boundary becomes the event you named, instead of the event that happens to precede it.

Agreed on the rest, including the honest gap. The append-only side is doing more work than it looks like, because it's the only one of the two where the question "did this stop happening" and the question "is it happening too often" read from the same record.

Thread Thread
 
pm25coder profile image
pm25coder

Fair hit — and it lands on a real boundary in the implementation rather than in the story. Let me answer with the code's actual event class, because it is not quite either of the two versions you tested.

The write is not literally "top of round." It sits inside the usage-anchor refresh, which runs as soon as the provider's usage arrives for an LLM exchange (daemon.py:2647 calls _refresh_usage_anchor, which calls _touch_planted_fire_marker at daemon.py:3327) — before that exchange's tool executions and before finalization. In a Q&A round with no tool calls, stream end is round end, so touch ≈ completion. In a multi-exchange round it fires once per sub-round: each LLM exchange touches the file as soon as its usage lands, with the tools that follow still unexecuted. So the marker's real event class is "an LLM exchange completed with usage received," not "a user round completed."

Your crash-loop race is real in exactly the window you named: a crash that lands between the anchor refresh and the end of that round's remaining work refreshes the marker each iteration, and the stale alarm never fires — the marker then ages only if the restart lands before the touch.

The sharper boundary underneath it is that the reader is in-process. _check_planted_fire_stale runs on a 6h asyncio loop inside the daemon (daemon.py:3188-3198); the daily drill is another in-process task. During a genuine crash loop — the 131-restart case — the reader is dead along with the writer, and the marker file sits untouched until the process comes back up. Restart-immutability of a file only pays off when the reader lives outside the process (a supervisor stat-ing the mtime), which is the half of the article's argument that is stated as design and not yet shipped as code. On our side that means the marker as built catches "daemon alive, but the LLM-exchange path stopped running" — not "daemon crash-looping," which nothing in-process can catch by construction.

On your fix — write at the bottom of the round — I agree it is right for the completed-round question, with one caveat about why the touch sits where it does. The marker doubles as the planted-fire detector's persisted last-heartbeat: the anchor-bias-heartbeat fires per LLM exchange, and the marker file is its persistence so the 6h alarm has something to read (daemon.py:3316-3317). The detector's event class genuinely is per-exchange. Folding "completed round" into that same file makes one file answer two event classes — which is the failure shape this whole thread has been circling. The cleaner shape is two files: keep the per-exchange marker as the detector's heartbeat, and add a separate completed-round timestamp written at finalization (final answer, or the no-more-tool-calls branch that ends the round). Then "how long since a completed round" reads the second file, and your crash-loop test passes against it by construction. It is also a strict improvement for the alive-but-stuck case you called a wash: today a hang inside tool execution keeps the marker fresh (last touch = last LLM exchange), so it is invisible until recovery; with a completion write, that hang ages the file immediately.

Honest gap stands as before: neither the rate signal nor a completed-round timestamp is in production — the raw material is on disk, the alarms are not. This exchange is the strongest argument yet for closing that gap.