[Bug] v0.8.0: duplicate-snapshot byte mismatch closes a healthy worker's control channel and terminally fails the root session #1662
Replies: 2 comments
|
Update: both fix stages are implemented locally on v0.8.0 and validated against the original incident workload. Stage 1 - containment (stop bricking healthy workers)Patched the four fatal sites in
Cache eviction and client resync behavior is unchanged - only the escalation-to-worker-recovery is removed. Stage 2 - root cause (atomic capture of ID and content)Made the snapshot cut atomic so an ID always maps to one byte-stable state:
With this ordering, two encodings of the same cursor necessarily serialize the same frozen message array, so the supervisor's duplicate byte-check becomes a valid invariant rather than a latent false-positive. ValidationApplied to the installed v0.8.0 bundle and exercised against the exact workload that produced this incident (18 MB root transcript, 1,492 messages, 145 RLM children, goal-continuation auto-resume):
Caveat: this validation is operational, not yet a formal test suite. If maintainers are open to it, I'm happy to prepare a proper PR from source (not the bundle diff) with the regression tests sketched in the report plus a changelog fragment. |
|
@umyunsang do you have code pushed somewhere? the latest v0.8.1 is basically unusable to me |
Uh oh!
There was an error while loading. Please reload this page.
Summary
The bug reported in #1229 is still reproducible on v0.8.0. A snapshot cache byte mismatch ("Duplicate snapshot ... did not match cached bytes") escalates to closing the supervisor↔worker control channel of an otherwise healthy worker, which puts the resident worker through recovery, ends in terminal
lifecycle: "failed"("Waiting for a client with fresh runtime context"), and bricks the session: every subsequent open/resume of that transcript is rejected with:Meanwhile the worker process itself stays alive and keeps doing real work (model calls, subagents, transcript writes), invisible and uncontrollable from any client.
I also have a concrete ordering finding for why two encodings of the same
snapshotIdcan differ, beyond the hypotheses listed in #1229.Environment
Incident timeline (all times UTC, 2026-08-23)
From the supervisor log for the affected worker:
Worker descriptor afterwards:
{ "workerId": "ece296ed6458", "pid": 48829, "rootActiveSessionId": "9f5082ac63db", "sessionFile": ".../01a02a6e-952f-735d-bf03-efbceef68bfc.jsonl", "lifecycle": "failed", "consecutiveFailures": 2, "lastError": "Waiting for a client with fresh runtime context" }Key observation: the worker never died. The worker's own log contains only its startup line, zero errors. While the supervisor considered the session dead:
prime-agent listfroze at exactly 1,492 messages across repeated calls while the file grew by hundreds of KB — i.e. the supervisor's view of the worker was frozen at the moment the channel died.So this is purely the control/observation plane severing, then the recovery policy converting a display-layer cache problem into a terminal session failure.
Root cause: snapshot identity vs. content are captured non-atomically
snapshotIdis a logical event-cursor position used as a byte-level transfer identity:But the messages serialized under that ID are captured at a different time than the ID, and inconsistently so:
resultcomes fromawait this.createAttachResult(...)first;snapshotIdis computed after the await. If events land between readingmessagesinsidecreateSessionSnapshotand computing the ID here, the cached generation stores ID(seq=N) with content(seq=M>N).prepareReplacementSnapshot): the opposite order —snapshotIdis computed beforeawait this.createAttachResult(...). Same ID can now be paired with newer content on re-encode.createSessionSnapshot(daemon-mode.ts:5046) awaitsbuildRlmChildSnapshotsWithPassiveRlmSubagents(with up toMAX_SESSION_SNAPSHOT_STABILIZATION_RETRIES = 3stabilization retries) while the session keeps appending events, and returnsmessages: session.messages— a live array reference. The chunker only shallow-copies (const messages = [...options.messages], snapshot-transcript-cache.ts:34) and serializes lazily when consumed.Any of these makes two encodings sharing
(activeSessionId, eventGeneration, lastEventSequence)legitimately differ in bytes. The supervisor's duplicate validation then treats a benign capture-ordering artifact as a protocol violation:closeWorkerChannel=truecloses the private transport to the healthy worker,handleWorkerClosestarts recovery, and because a resident worker has no stored launch env / transient create command, recovery cannot relaunch it and marks it terminally failed after retries ("Waiting for a client with fresh runtime context"). The stale registration then blocks every future open of that transcript until a fresh create forcibly stops the (still working) process.Large transcripts amplify all three windows: serialization of a 15 MB history takes long enough that concurrent appends during capture become routine, which matches #1229's observation that affected sessions were the large, busy ones.
Proposed fix (staged, smallest first)
closeWorkerChannel=truefor snapshot-content mismatches; reserve channel closure for auth/framing/routing corruption.lastEventSequenceand materialize chunks under one synchronous boundary, retrying if the revision moved), or allocate a unique opaque transfer ID per materialization while keeping(eventGeneration, lastEventSequence)as the base position metadata. The latter is wire-compatible since clients treatsnapshotIdopaquely.Step 1 alone converts this from "bricks a live session" to "client resyncs", which matches what the mismatch actually is.
Suggested regression tests
Notes
All reactions