fix: isolate concurrent Pi replays and committed tool caches - #945
Merged
Conversation
…870) Oh My Pi drives one conversation from several callers at once: the main turn, title generation, and side questions asked mid-turn. All of them carry the same metadata.user_id.session_id, because the protocol has no per-flow signal to send. Turn coordination serializes them correctly, but whichever one lost the commit race arrived holding a branch the mapping had moved past, and was refused with a 400. That 400 is a hard error on the client side. In omp it triggers a model fallback, so a turn that could have run switched models and threw away the prompt cache instead. Adapters can now declare that their clients share one session key across concurrent turns, which routes them into the existing declaresConcurrentFlow escape that fork and subagent sources already use. The loser is answered by replaying its own history: one prompt-cache miss instead of a failed turn. Nothing else changes, and every other adapter still refuses a stale arrival. Admitting such a turn is not the same as trusting its lineage. A short side call carries a prefix of the main history, so after the main turn commits the loser reads as an undo against it, and honouring that would resume and rewind the session that just committed. A loser admitted by the protocol declaration is therefore reclassified as diverged before the undo path sees it. Per-request fork and subagent signals keep their lineage, since those callers name their own session boundary. Two tests cover it. The first reproduces the reported shape, two branches sharing a prefix and differing at the last message, and fails on main with the request never reaching the SDK. The second commits a winning turn with SDK message UUIDs while a prefix-carrying side call waits, and fails without the reclassification, with resume pointing at the winner's session.
The undo downgrade added for the pi adapter applied to every request that adapter serves, including the ones that carry their own concurrency declaration. A fork source or subagent signal names its own session boundary, so an undo from it is deliberate and honouring it is the point; only pi's unmarkable side calls need their arrival-order undo replaced by a replay. Gate the downgrade on the absence of a per-request signal. Also corrects docs/agents.md: the loser misses the prompt cache, it does not cost a hit.
rynfar
marked this pull request as ready for review
September 4, 2026 21:54
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.
Oh My Pi sends its main loop, title generation and side questions under one session ID. A request queued behind another caller could receive a hard 400 and trigger model fallback. Allow the Pi adapter to declare this concurrency pattern, serialize the requests, and replay a conflicting late caller from its own complete body. Explicit fork/subagent signals retain their intended undo behavior; other adapters retain stale-request protection.
Incorporates #922's two original commits with Mate Remias's author and authored dates preserved, followed by separate maintainer corrections and validation.
Real Oh My Pi 18.0.3 validation exposed another required correction: when the main request ran first, the title request inherited cached read/write tools and returned a read call instead of a title. Cache tool declarations only after successful durable publication and associate them with that exact SDK session. Fresh requests use their own declarations, empty fresh branches do not inherit old tools, and failed side requests cannot poison the main branch's cache. The bounded MCP instance cache continues to reuse identical tool registrations.
Validation:
554b531134dfb092382383592356c2f6f19714e5: test, Windows smoke, Docker smoke and build (runs 33922862600 / 33922862613).The durable mapping follows the last completed caller. If a side call finishes last, the next main turn may also replay; the change does not promise a single prompt-cache miss or immunity from upstream errors. Headerless clients still have the existing cache-identity limitations. OpenCode-specific conflict proposals in #869 remain under separate review.
Fixes #870. Supersedes #922 with contributor credit.
Co-authored-by: Mate Remias materemias@gmail.com