DEV Community

Cover image for Do agents survive a crash, and why does an LLM retry execute the same side effect twice? 34 runs measured

Do agents survive a crash, and why does an LLM retry execute the same side effect twice? 34 runs measured

sunnydachs on September 23, 2026

When you hand an agent work with external effects, two worries always come up. First: when the process dies, what happens to the work in progress?...
Collapse
 
tokenlat profile image
TokenLat •

The double-fire you caught is the same seam I keep hitting: the (step, tool, model) tuple quietly assumes steps are read-only. The moment a step mutates state, cost attribution and retry semantics split apart — a retry of a write is a duplicated action, not a duplicated cost. Content-hash keys stop the same call, but they don't tell the router a step is side-effecting in the first place. I'd want a step-level effect tag the retry layer reads before firing, so "cheaper path" can't mean "double side effect." Do you tag read-vs-write at the step, or leave it to the caller's retry loop?

Collapse
 
sunnydachs profile image
sunnydachs •

Thanks — agreed on the seam. In this harness the tag lives at the tool boundary rather than the step: only the destructive publish routes through the idempotency ledger, the read tool never touches it, and the key is caller-assigned (one position key per run). There is no retry layer between router and tool here — the retry being measured is the model's own re-reasoning after a transient directive, so a router reading a step-level tag would never fire; the guard sits where the effect executes. The case you describe — "cheaper path" becoming a double side effect — is what the position key measured: same position + reworded args was refused as a caller bug (8 refusals across 3 runs, one publish per run) instead of silently reusing the key.

Collapse
 
tokenlat profile image
TokenLat •

Putting the guard at the tool boundary rather than the step is the right call for your setup — a step-level tag the router reads would never fire here, since there's no retry layer between router and tool and the retry you're measuring is the model re-reasoning after a transient. So the effect-execution point is the only place the guard can actually act.

The caller-assigned position key refusing "same position plus reworded args" as a caller bug is a nice catch — that's the double-side-effect case made visible instead of silent. The question it leaves me with is ownership: when that refusal fires, does it come back to the model as an error it can recover from (rewrite the call, pick a new position), or does it just block and hand the problem to a human? If the model can't see why the key was refused, the guard protects the system but the agent stalls — so which way did you land on it?

Thread Thread
 
sunnydachs profile image
sunnydachs •

Thanks — the traces answer it: the refusal comes back to the model as a tool result, not to a human. The tool returns the reason with it — same key, different parameter hash — and the model sees it in its next request. What it can't do is pick a new position — the key is assigned by the caller, so its only lever is the arguments.

Over the 3 reworded runs: 8 refusals, one publish per run, one SUCCESS row per ledger. The one recovery measured: re-issuing the original arguments — the recorded result came back, no second execution; the other two runs ended with the model reporting the first publish.

Collapse
 
hannune profile image
Tae Kim •

Cell B hit me first because I'd have reached for sha256(args) too. It's intuitive but fragile: an LLM under timeout pressure re-reasons from a context that now includes the error, so the tool arguments come out reworded and the hash changes. Position key wins because it doesn't care what the model said, only where in the workflow it is. Worth testing next: does field-ordering variance from the JSON serializer alone produce false mismatches, even without any semantic rewording?

Collapse
 
sunnydachs profile image
sunnydachs •

Thanks! Great question — the traces already hold part of the answer: field-order variance is real (a pair of identical calls hashed to two different sha256 values), but no false mismatch within a single run yet — the position key never hashes args at all.