You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(service-automation): re-seat the suspension map entry when the durable save fails (#16216)
* fix(service-automation): re-seat the suspension map entry when the durable save fails
A concurrent per-id `loadSuspendedRunStrict` landing inside `persistSuspendedRun`'s
save window evicts the live map entry (#16129's base window, which stays as pinned).
Compounded with the save then FAILING, the run was left with neither a durable row
nor a map entry: `hasSuspendedRun` answered `false` and `resume` answered
`RUN_NOT_FOUND` — the run lost in-process, not merely un-durable — while the engine's
own `error` record told the operator it was "kept in memory only" and that they had
until the next restart to act.
The catch now re-seats the map entry alongside the cache-only marking, so the marking
qualifies something again. Option C of the card: the cache-only marking is not
widened (that would weaken #13617's store authority), no lock is added, the save is
not reordered, and the base window is untouched.
The operator record is corrected in the same seam: it keeps its promise, and now
names the two reads (`hasSuspendedRun()`, `listSuspendedRuns()`) that must answer for
the run, so the promise can be falsified instead of trusted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
* chore(changeset): the failed-save re-seat and the corrected operator record
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
* docs(service-automation): retire the stale "escapes those bounds" claim on evictConsumedSuspension
The compound case that paragraph pointed at is closed by the re-seat in
`persistSuspendedRun`'s catch, and the pin file now records the intended outcome
rather than the measured loss. Comment-only.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
---------
Co-authored-by: Claude <noreply@anthropic.com>
A suspended run whose durable save fails is now kept resumable in this process even when a concurrent read landed mid-park — and the error record that reports the failure says how to check that.
6
+
7
+
`AutomationEngine.persistSuspendedRun` writes its map entry BEFORE it awaits `store.save()`, and marks the run cache-only only once that save settles. For the whole of that await the entry is live but unqualified, so a concurrent per-id `hasSuspendedRun` / `resume` reads a store that truthfully has no row yet, finds no qualifier, and evicts a run that is being parked right now. That window is bounded and stays as it was: the strict load is store-first, so once the save lands the run is resumable from the store, and only the cache-only listing under-reports.
8
+
9
+
Compounded with the save then **failing**, it was not bounded. The catch marked the run cache-only, but the map entry that marking qualifies had already been evicted, so the run had neither a durable row nor an in-memory copy: `hasSuspendedRun` answered `false` and `resume` answered `RUN_NOT_FOUND`. The run was lost **in this process**, not merely un-durable — for example a paused approval that no decision can ever advance. Reaching it needs a store that rejects the write while still answering reads with "no row" rather than throwing: a healthy read replica behind a broken write path, a missing `INSERT` grant, a full disk.
10
+
11
+
-**The failure path now re-seats the map entry** alongside the cache-only marking, so the marking qualifies something again and the documented degradation — a failed save costs cross-restart durability, not in-process resumability — holds in this interleaving too. The cache-only marking is not widened, no lock is added, and the save is not reordered, so a run is still never readable out of the map while the store is authoritative for it.
12
+
-**The error record for a failed save is corrected.** It kept telling the operator the run was "kept in memory only" and that they had until the next restart to act, which in this interleaving pointed away from the loss: the run was already gone, and the restart would take the blame. It now names the two reads that must still answer for the run (`hasSuspendedRun()` and `listSuspendedRuns()`), so the promise can be checked rather than trusted. It still reports the same cause in the same structured slot, at the same `error` level.
0 commit comments