fix(confirm-write): approve one write at a time per conversation - #28
fix(confirm-write): approve one write at a time per conversation#28jerelvelarde wants to merge 1 commit into
Conversation
A model turn can emit several tool calls at once, and LangGraph runs each as its own task. Two mutating calls in one turn therefore reach copilotkit_interrupt in the same super-step and leave two pending interrupts. Nothing can answer that: the Channel posts one card, and resuming without an interrupt id raises "When there are multiple pending interrupts, you must specify the interrupt id when resuming", which escapes as an ASGI error and kills the run. Reproduced against LangGraph directly -- two parallel tasks that both interrupt leave two pending, and a bare Command(resume=...) fails exactly as it did in production. A second turn arriving while a card is still unanswered does NOT stack, so the accumulation is within one turn only. Let only the first write on a thread pause for approval. The rest come back unrun with a result telling the model to re-issue them, which it does on the next turn, so the writes queue up behind one card at a time instead of racing for the same resume. The claim is keyed by the call itself so it survives the replay that resume performs, and it is released only when an approval resolves -- approved or declined. The gate is shared across interceptors because each MCP server gets its own: two writes to different servers in one turn is precisely the case a per-server gate would miss.
99d338e to
1dd2129
Compare
|
Correction to the "main is red" note in the description above — that was wrong, and it was my environment.
So there's nothing to chase in #15/#16 on that account, and the "pre-existing failure" I mentioned in #23 wasn't real either. Sorry for the noise. Rebased onto (The one |
Follow-up to #23 (merged). Fixes the second production failure in the same approval gate.
The error
It escaped as an ASGI error and killed the run.
Cause
A model turn can emit several tool calls at once, and LangGraph runs each as its own task. Two mutating calls in one turn therefore both reach
copilotkit_interruptin the same super-step and leave two pending interrupts. Nothing can answer that: the Channel posts one card, and a bareCommand(resume=...)is rejected outright.I reproduced both candidate paths against LangGraph directly:
So the abandoned-card theory is out: a new turn replaces the pending interrupt rather than stacking. The accumulation is strictly within a single turn.
Fix
Only the first write on a conversation may pause for approval. The rest come back unrun, with a result telling the model to re-issue them — which it does on the next turn. Writes queue behind one card at a time instead of racing for the same resume.
Verification
End to end with the real interceptor inside a real graph, real
interrupt(), two different MCP servers:77 agent tests pass (7 new): parallel writes deferring, a replayed call reclaiming its own pause, release on both approve and decline, cross-server sharing, reads never deferred, and — with no thread id available — the write still gated rather than silently executed.
Heads-up: main is currently red on the TS side
Not caused by this PR, which is Python-only. On
mainat 9478b3c, four test files fail to load:The chart work in #24/#26 needs a newer
@copilotkit/channelsthan the installed tree —package.jsondeclares0.6.1,node_moduleshas0.2.2-canary.rc-1. Same skew that makespnpm check-typesreport 4 errors andapp/cleanup.test.tsfail. Probably wants the dependency bump in #15/#16 to land.The step-budget /
GraphRecursionErrorfix from the other log is separate, in #27.