Repository navigation
Run one session's hooks one at a time, and retry a blocked read - #156
Merged
Saksham Uboweja (SakshamUboweja) merged 1 commit intoNov 6, 2026
Merged
Saksham Uboweja (SakshamUboweja) merged 1 commit into
Saksham Uboweja (SakshamUboweja) merged 1 commit into
Conversation
An agent that calls tools in parallel starts their hooks together. Each reads the session document, folds its call in and writes it back, so the last writer won and the loop guard's turn lost 17-21 of 45 calls in the repro in microsoft#142. On Windows a read during that race raised PermissionError, which Store.read took for a missing document, so the next save could write an empty state over the real one. - Store.read retries a blocked read on Windows, as Store.write does since microsoft#140, and returns the default only for a missing file or bad JSON. - hookqueue runs a session's hooks under a per-session lock kept outside repository state. A hook that gets no lock in time queues its event for the holder and answers {}. SessionStart, and UserPromptSubmit under repomap=on, answer with context, so they wait and then run. - A queued hook has already answered, so a guard decision reached on a replayed call is held on the turn, uncounted, and the turn's next call acts on it. A turn that ends first drops it as an `undelivered` event. Fixes microsoft#142 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
@microsoft-github-policy-service agree |
Saksham Uboweja (SakshamUboweja)
merged commit Nov 6, 2026
6378a58
into
microsoft:main
10 checks passed
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.
Fixes #142. Based on
mainafter #140, following the plan in the issue.What changes
Store.readretries a WindowsPermissionErrorthe wayStore.writedoes since Windows: retry a blocked state save; find sh in the Codex tests #140, and returnsdefaultonly for a missing file or bad JSON. Any other failure now raises, and the hook fails open as usual._replaceandreadshare a small_retryinghelper.plugin/hookqueue.py(new) runs a session's hooks under a per-session lock, kept in<root>/locks/<host>/outside repository state. A hook that gets no lock in time saves its payload to the session's queue and answers{}. The lock holder applies the queue after its own event, and every later hook applies it before its own.SessionStartis never queued.store.pydocstring no longer says a session document needs no lock because one session writes it.tests/test_hook_queue.py::test_tool_calls_made_in_parallel_are_all_recorded.What happens to a nudge from a queued
PostToolUseThe hook that queued the call has already returned
{}, so nothing can deliver a redirect on its behalf. Our local version of this patch got that wrong, which your question caught: replaying the call ran the whole decision, so the nudge was spent, the segment reset and thenudgeevent logged, and then the answer was thrown away.Here a decision reached on a replayed call is not acted on:
post_toolstores it on the turn (TurnGuard.held).foldstops deciding while one is held, so a burst of queued calls gives one decision.PostToolUseorPostToolUseFailureacts on it through the normal path. The budget, the log,nudge_bodyand the notice all happen there, so the answer carries that hook's own event name, which Claude Code requires. The stop notice works the same way._close_turndrops it and logs anundeliveredevent. No nudge is counted, and there is no one left to redirect.So a redirect can arrive one call late, and it is lost only when the turn ends on the queued call. For that to matter a hook has to wait out the full 3 s. An earlier version of this patch ran on one Windows machine for about a day of real sessions, and no hook had to queue.
Two other answers a queued hook would lose
UserPromptSubmitunderrepomap=oncan carry the repository map, so it is not queued in that mode. It waits, then runs without the lock, likeSessionStart. With the map off it always answers{}.SessionEndthat no later hook applies is picked up bypending_sessions()at the next session start, so the session still gets folded.Waits
Each wait stays short of the shortest timeout any agent gives that hook, and a test reads
plugin/hooks/*.jsonto pin it. Codex allowsSessionEndonly 3 s, so that wait is 1 s. Our local version waited 5 s, which would overrun it.SessionStartUserPromptSubmit,StopPostToolUse,PostToolUseFailureSessionEndResults
The repro from the issue on Windows, 5 rounds × 9 calls. It is a race, so
main's numbers are the range over three runs.errors.logentriesmain@ a71684c (with #140)Real headless Claude Code 2.1.317 sessions on Windows, with the plugin built from each side and haiku making 8 parallel
Readcalls in one message, 2 runs each:main@ a71684cTests
main@ a71684ctests experiments/teststestsThe failing tests are the same on both sides of each row. On Windows, the 39 are the known failures in
experiments/tests. The Linux container has nogit, which is the error its failures show, so CI is the real check there.We also broke the code on purpose in a copy, to confirm the new tests notice:
defaultagain, 2 fail.SessionEndwait, 1 fails.Not in this PR
os.replacecalls outsidestore.pyhave no retry. Our local patch retried them all, but the plan here coversStore.readandStore.write.Store.read_linesstill returns[]on anyOSError.PostToolUsereplayed after a newer prompt has opened its turn closes that turn as one we never saw end, as any out-of-order hook does today. It takes a hook that waits out its 3 s across a new prompt.🤖 Generated with Claude Code