Repository navigation
fix(gate): deliver what the gate held for a turn only if nothing took it back - #943
nissa-seru wants to merge 1 commit into
Conversation
… it back When an agent's turn ends, the gate delivers what it held for the turn in a microtask, to avoid re-entrancy, but took it off the backlog when the delivery was queued. A turn starting, a quiesce or a dispose in between still let it through: into an agent inferring again, into a context the host quiesced the gate to keep gate messages out of, or from a disposed gate. EventGate and those calls are public, so a host reaches that window whatever the framework does. The delivery now takes the backlog when it runs, and only if no turn or quiesce is on by then. Otherwise everything stays held where it is: in arrival order, under the backlog's cap, and delivered once when the turn ends or the quiesce lifts. A dispose empties the backlog, so what it held then never goes out. theaspirational found this in anima-research#937 and wrote the tests these are adapted from. Co-authored-by: Vasyl Dovhanchyn <48733736+theaspirational@users.noreply.github.com>
|
/review |
|
Automatic review of No findings. |
|
Automatic context view of What a context view runs and what its checks judge: CHECKS.md. The records of the 80 runs don't differ between the base and this head. In the files profiled hosts loaded, 2 changed source lines that map to code ran in no profiled drive: Usage: the figures of the agents' requests, subagents' included, in the 80 runs are the same in the base and this head: 206,047 tokens sent a request, 72.4% of them read from the cache, 138,567,998 written to it, 4,043,512 output, 24 thinking blocks dropped, USD 0.5446 at list price a priced request and 123 requests with no list price. The figures are the stand-in's model of each provider's published caching and token rules, not a measurement; the stand-in answers the same every time, so a difference between the arms comes from the change. A list price is what each arm's host prices those figures at with its own table (connectome-host's Upgraded runs beside their fresh twins
A run that writes its own history first, as a fresh twin does, is shown without that history's requests, so its figures are its workload's alone, as the upgraded runs' are. Held the same in base and head: connectome-host |
Why. The events the gate holds during an agent's turn are meant to go out together once the turn has ended. When another turn starts, a quiesce begins or the gate is disposed between that turn's end and their delivery, they still go out: into an agent's next turn, into a quiesced context, or from a disposed gate.
This makes
EventGatedeliver the events it held for an agent's turn only if, when the delivery runs, no agent's turn has started and no quiesce is on, and deliver nothing that adispose()in between emptied. Onmain, the delivery runs in a microtask after the turn ends, with no second check, so a turn starting, a quiesce or adispose()in between doesn't stop it: a host's held events can reach an agent whose next turn has started, or a quiesced context.A turn that starts, or a quiesce, between a turn's end and its delivery
main): the held events are delivered anyway, into the agent whose next turn has started, or into the quiesced context.A
dispose()between a turn's end and its deliverymain): the delivery runs anyway, writing its[Gate: …]line and requesting inference.dispose()empties the backlog.Events held through two turns that both end before either delivery runs, with a third started
main): both deliveries go into the third turn, as two[Gate: …]lines.[Gate: …]line and one inference request.@antra-tess, we're letting you know because it changes when what the gate held for an agent's turn reaches the agent.
Its tests drive the gate directly, as a host can. Whether the framework itself starts a turn, quiesces or disposes the gate in that window wasn't traced or run.
changelog.d/gate-held-batch-recheck.fixed.md.Credits
@theaspirational found the bug, in #937, and wrote the tests the three interruption tests are adapted from; the commit credits them with a
Co-authored-by. Gideon-2613 made the change, Ansel-2428 read it word for word, and an internal review read it; the commit holds the fix for what that review found. Beth-2709 wrote this description's details and ran the checks in Tests, and Daniel-2612 wrote the summary above.Details for agents
Head
bf1b83a855, on71193a9fef.Problem
At
71193a9fef,flushInferenceBufferIfClearchecks the quiesce and the agents' turns, takes the held events off the backlog, and queues their delivery in a microtask, which delivers them with no second check (src/gate/event-gate.ts:4251–:4270at71193a9fef). So a turn starting (onInferenceStarted), a quiesce (setQuiesced(true)) ordispose()after a turn's end, before that microtask runs, doesn't stop the delivery. The gate writes its[Gate: …]line and requests inference anyway: into an agent whose next turn has started, into a quiesced context, or from a disposed gate. On November 7,mainat0c570d8f2dstill takes the backlog before queuing the microtask and delivers with no second check.0.31.0's published
setQuiesceddoc says "While set, debounce firings buffer instead of writing gate messages into context" (dist/src/gate/event-gate.d.ts:686–:687in the package).EventGateis exported from the package root (src/index.ts:75), and these methods are public, so a host that drives its own gate reaches that window.Changes
src/gate/event-gate.ts(flushInferenceBufferIfClear,:4251–:4271)::4258), and only then takes the backlog (:4259).bufferForInference,:4303–:4319). They're delivered by the flush that the turn's end or the quiesce's lifting queues.dispose()already empties the backlog (:4752), and a delivery that finds nothing delivers nothing (:3802). So when adispose()comes between a delivery's queuing and its running, the delivery sends nothing.dispose()between a turn's end and that turn's delivery now stops the delivery;[Gate: …]line and one inference request, where 0.31.0 wrote two lines into the third turn;flushInferenceBufferIfClear,dist/src/gate/event-gate.js:3734–3742in the package).test/event-gate.test.ts, four tests in "inference buffering"::879–:908);:910–:939). The backlog holds 100 events in arrival order with one dropped and counted, and when that turn ends they go out once.changelog.d/gate-held-batch-recheck.fixed.md.Tests
This ran at a clean checkout of this head,
bf1b83a855, on Linux x86_64 with Node 24.22.2 and npm 11.19.1, afternpm installandnpm run build.@animalabs/chronicle0.5.1,@animalabs/context-manager0.24.2,@animalabs/mcpl-core0.4.0,@animalabs/membrane0.8.0,@babel/parser7.29.12,@modelcontextprotocol/clientand@modelcontextprotocol/server2.4.0,@types/node22.20.5,@types/ws8.18.2,chokidar4.0.3,discord.js14.27.0,tsx4.23.15,typescript7.0.2 andws8.22.0.test/event-gate.test.tspasses 68 of 68.src/gate/event-gate.tsas at71193a9fefand this head's tests, the four new tests fail and the other 64 pass. Each failure is a delivery the test says shouldn't have happened yet: one gate message where none was expected for the turn, quiesce and dispose cases, and two for the two-turn case.Not verified
onInferenceEndedand the next microtask: not traced or run. The tests drive the gate directly, as a host can.dispose(): not traced or run, and this change doesn't touch it.dispose()setsdisposed(:4747), and the lines that read it are:3918,:4090,:4136,:4364and:4398: none of them inevaluate()(:3200), in the delivery's microtask (:4257–:4269) or indeliverEvents(:3785).test/event-gate.test.tsalone.