Do not report an existing session as missing in getOrCreateSessionHolder - #119
Merged
Ololoshechkin merged 1 commit intoAug 11, 2026
Conversation
`getOrCreateSessionHolder` looked the session up once, before entering the
`_sessions.update` CAS loop, and the loop's `initializingSessionsCount == 0`
branch never consulted `currentStorage` again. A `session/update` handled on
the protocol read loop while a concurrent `newSession`/`loadSession` was both
registering its holder and decrementing the counter back to zero therefore
took that branch, and the notification failed with
AcpExpectedError: Session <id> not found
even though the session existed. The update was dropped, and clients that wait
for one - a session-setup probe waiting for `available_commands_update`, say -
stalled until their own timeout.
Agents that emit an update immediately after the `session/new` response hit
this in the field: with claude-agent-acp the response and the notification
arrive in the same read, and the notification loses roughly one session in
tens of thousands.
Look the session up inside the atomic block, so every branch - including the
retry after a failed CAS - sees the map as it is at that moment. The read
before the loop stays as a fast path for the common case.
Ololoshechkin
approved these changes
Aug 11, 2026
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.
Problem
Client.getOrCreateSessionHolderreads the session map once, before entering the_sessions.updateCAS loop, and the loop'sinitializingSessionsCount == 0branch never consultscurrentStorageagain:_sessionscan change between that read and the CAS, and again between two attempts of the CAS loop. When asession/updateis handled on the protocol read loop while a concurrentnewSession/loadSessionis both registering its holder and decrementing the counter back to zero, the notification lands in theelsebranch and fails witheven though the session exists. The update is dropped.
How it shows up
Agents that emit an update as soon as the session exists hit this in the field. With
claude-agent-acpthesession/newresponse and the firstavailable_commands_updatearrive in the same read:A sibling probe against the same agent, whose notification happened to arrive 2 ms after the response, was unaffected. The caller here was waiting for
available_commands_update, so the dropped notification cost it a full grace period and it ended up with an empty command list.Fix
Look the session up inside the atomic block, so every branch — including a retry after a failed CAS — sees the map as it is at that moment. The read before the loop stays as a fast path for the already-registered case.
Test
ClientSessionUpdateDeliveryTestdrives a fake agent that answerssession/newand, without yielding, sends onesession/updatefor the session it just created, withnewSessionrunning off the protocol read loop's thread.The interleaving is only reachable under real parallelism, so this is a probabilistic stress test rather than a deterministic one: correct code can never fail it, while on
masterit fails in roughly one run out of three on a warm 10-core laptop, always withAcpExpectedError: Session session-<n> not foundand one lost update. Increasing the attempt count does not help much — the hits cluster rather than distribute uniformly — so it is sized for ~1.7 s.:acp:jvmTest,:acp-model:jvmTestand:acp-ktor-test:jvmTestpass.