fix(session): serialize state read-modify-write to prevent lost updates#55
Merged
Conversation
session.ingest -> #withState does an async load -> mutate -> save. The adapter's own hooks coalesce via a `pending` flag, but once callers drive evaluate() / ingest() from their own navigation signal (as the content docs now recommend), two evaluations can overlap: both load the pre-write snapshot and the second save clobbers the first, dropping a just-recorded stand-down. That can flip a host from standDown:true back to false — an activation on an already-attributed page. Add a FIFO mutex (#serialize) and route #withState, #failClosedWithAudit, and exportAuditLog through it, so state operations run to completion in order. Also gives read-after-write consistency: a shouldStandDown queued after an ingest now observes that ingest's committed state. Adds a concurrency test (verified to fail without the lock: one host's record is lost; passes with it). Addresses item 2 of #52. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PXZmuMFkq8dE2e2KLXvitx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses item 2 of #52: concurrent
evaluate()/ingest()calls could lose-update the session store.The race
session.ingest→#withStatedoes an async load → mutate → save (src/session.ts). The content adapter's own hooks coalesce via apendingflag, so through them evaluations never overlap. But #51 now recommends adopters drivecontroller.evaluate()from their own navigation signal, and those calls are not coalesced. Two overlapping evaluations both load the pre-write snapshot; the secondsaveclobbers the first, dropping a just-recorded stand-down record. Worst case that flips a host fromstandDown: trueback tofalse— activation on an already-attributed page, the exact thing standdown exists to prevent.Fix
A small FIFO mutex on the session:
#serialize(task)chains onto a#stateLockpromise so each state operation runs to completion before the next starts.#withState,#failClosedWithAudit, andexportAuditLogall run inside it.Safe against deadlock: every
fncallback passed to#withStateis synchronous and self-contained, and no serialized method calls another. Bonus: read-after-write consistency — ashouldStandDownqueued after aningestnow observes that ingest's committed state instead of possibly loading a snapshot from before the save landed.Test
New test fires two overlapping
ingests (different hosts) through a store that yields on every load/save to force interleaving, then asserts both records persist. Verified it fails without the lock (one host's record is dropped:expected undefined to be defined) and passes with it.Verification
typecheck+lintclean, 163 tests pass (was 162 + the new one),policies-cite-checkpasses,buildok. No API change; behavior change is limited to ordering.Leaves item 3 of #52 (session-exemption TTL / value-scoping) as the remaining open design question.