Repository navigation
fix(framework): a module's event after stop() began is dropped with a log line, not thrown - #316
Merged
nissa-seru merged 3 commits intoOct 14, 2026
Conversation
added 3 commits
October 13, 2026 07:49
… log line, not thrown stop() closes the event queue first and stops modules last, after the streams, the event loop and typing. In between, a module is still running: a listener (a Discord message, a workspace pass's fs event) can fire, and a module's own stop() can push a final event. On main both of a module's paths threw "Queue is closed": ModuleContext.pushEvent() reaches AgentFramework.pushEvent(), which rethrows for anything but a tool result, and the deprecated ModuleContext.queue is the raw queue. Thrown from a listener, that's an unhandled exception or rejection; thrown from a module's stop(), it made the framework's stop() reject. Nothing will process such an event, so it's now dropped with a log line naming the module and the event type: [framework] a '<type>' event from module <name> arrived after stop() began — dropped A direct caller of AgentFramework.pushEvent() still gets the throw, as anima-research#263 decided: it can catch it, and learns the framework is stopped. The module context's pushEvent() and its deprecated queue handle both go through pushModuleEvent(), which names the module. ModuleRegistry's queue and pushEvent option take the module's name as an optional second argument (ModuleQueue), so a ProcessQueue and a one-argument callback still fit. The queue handle stays untraced, as before; pushEvent() traces process:received. Not changed here: a start() after a completed stop() leaves the queue closed. Reopening it would need old lifecycles' module contexts and late tool results kept out, and the push coalescer and liveness watchdog restarted. Tests (test/late-events-after-stop.test.ts, 4): a module's events through both paths while running (only pushEvent() traced); a module pushing from its own stop() through both paths: dropped and logged, and stop() completes; a module pushing after stop() finished: no throw, logged; a direct pushEvent() after stop() still throws. On main, the second and third fail (the second with stop() rejecting "Queue is closed").
…ne that names its call; docs say what a late module event becomes Answers a review of the module-event drop. - A tool result a module pushes after stop() began got the generic module line, losing what anima-research#263's drop line gives an operator reconciling the call: its id, the tool, and whether it succeeded. pushModuleEvent() now hands a tool result to pushEvent(), whose own drop logs that line, and drops anything else with the module line. A test pushes a tool result through both module paths after stop(): each logs anima-research#263's line. - ModuleContext's pushEvent() and queue docs, in both of the context types that carry them, say a push after stop() began is dropped with a log line naming the module, untraced. - The from-stop test runs two modules, so each drop line is shown to carry the module that pushed it. - Changelog: the tool-result line, and that the deprecated ModuleContext.queue is now an object with push alone, as its ProcessQueue type always said; it was the framework's own queue object.
…d that a direct caller's throw excludes a tool result The doc comment on pushModuleEvent() and the changelog fragment said nothing could catch a module's throw. From a listener it can go unhandled, and from a module's stop() it failed stop(); a caller wrapping a callback could still catch it. The fragment also says a direct caller of AgentFramework.pushEvent() gets the throw for anything but a tool result, which anima-research#263 drops with a log line. No code change.
|
Contributor
Author
|
We'll merge this once our pre-merge checks pass on its merge with the then-current CI is green at — Ada-1017 |
This was referenced Oct 14, 2026
Merged
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 14, 2026
…er-deadlines Main now carries anima-research#278, anima-research#254, anima-research#316, anima-research#318, anima-research#211, anima-research#246, anima-research#303 and anima-research#249 past dff7fc2. One textual conflict, in src/framework.ts, the adjacency Ada named (#88423): after PROVIDER_ACCELERATION_JITTER_MS, this branch's MAX_TIMER_DELAY_MS and PROVIDER_WAIT_ALERT_MS and anima-research#249's isMembraneError() are all kept, as in the readers' stand-in (8028482).
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 14, 2026
…search#290 Takes anima-research#290 (a context budget the agent reached comes back reached), and what landed since 3b28ee7: anima-research#254, anima-research#316, anima-research#318, anima-research#211, anima-research#246, anima-research#303, anima-research#249, anima-research#279, anima-research#251, anima-research#267 and the 0.22.1 release (anima-research#328). One conflict, in Agent.compileWithInjections, resolved as both pull requests' merge notes give it (room-203 #86391, #89240): - anima-research#290 sends a compile without a budget through compileAtLiveBudget, which settles the runtime transition, notes the compiled budget, and on an OverBudgetError restores a budget the boot applied at once as a paced descent. A dry run proves nothing about the budget: it goes straight to the compile, around compileAtLiveBudget, so it settles no descent, notes no compiled budget and restores nothing, whether it fits or is refused. This branch's settle line, which skipped a dry run, goes: anima-research#290 moved the settle into compileAtLiveBudget. - The watermark stays as this branch had it: a dry run moves none. The next commit tests a dry run under a pending at-once restore.
nissa-seru
pushed a commit
that referenced
this pull request
Oct 14, 2026
main gained #278 (workspace sync says what it passed over), #254 (R17 recovery transitions) and #316 (a module's event after stop() began is dropped with a log line, not thrown). It merges without conflicts. #316 decides by the closed queue, as this change's awareness sync does; the operator entry points also refuse while a stop() attempt is in flight.
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 14, 2026
Main since 197a849 brings anima-research#310 (module results that can't be stored), anima-research#283, anima-research#278, anima-research#254, anima-research#316, anima-research#318, anima-research#211 (placed reaction baseline), anima-research#246, anima-research#303 (references fail closed), anima-research#249, anima-research#279, anima-research#251, anima-research#322 (close info on a connection's 'close'), and the context-manager ^0.14.0 and membrane ^0.5.87 floors. Conflicts, each resolved to keep both sides: - package.json: main's floors, plus this branch's @modelcontextprotocol/client. - src/index.ts, src/mcpl/index.ts: this branch's exports plus main's McplConnectionCloseInfo and TransportCloseInfo types. - test/mcpl-ws-transport.test.ts: both sides' imports. - server-connection.ts: this branch's close attempt (closeStep, which already carries anima-research#221's wait for an aborted reconnect's own cleanup), with anima-research#322's close info on the 'close' it emits, and anima-research#322's hostCloseRequested field. - framework.ts imports: the union of both sides (anima-research#303's reference helpers, anima-research#211's REFUSAL_REACTION_BASELINE and isWebSocketTransport). - framework.ts: this branch's unconvertibleScriptResult beside main's failure-text bounds (anima-research#249), which replaced truncateReason. - framework.ts fields: mcpServerAdmissionClosed and anima-research#316's lifecycleGeneration. - stop(): main's wait for guard annotations, then both engines' servers. - autoFetchReferences: anima-research#303's register-then-judge-each-uri-once pass, with this branch's script tail; a uri the result calls expired is fetched for neither. - handleScriptToolCall: anima-research#310's text for a non-error result with no data (unless structured), and this branch's dispatch-time fetch for scripts in place of main's fetch at conversion. - syncDiscordAwarenessMarkers: main's doc comment after this branch's modern-connection methods. - The legacy connect: anima-research#211's derived env for a stdio child, then this branch's unreaped-launch and admission checks on that connection. - restartMcplServer: anima-research#211's ledger read joins the checks made before anything is torn down (and before the restart claims its id and prefix).
This was referenced Oct 14, 2026
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 14, 2026
Main since dff7fc2: anima-research#278, anima-research#254, anima-research#316, anima-research#318, anima-research#211, anima-research#246, anima-research#303, anima-research#249, anima-research#279, anima-research#251, anima-research#267, the 0.22.1 release (anima-research#328), anima-research#290 (81b8521) and anima-research#285 (5346605). Two conflicts, both in Agent's compile methods, resolved as this PR's merge notes said: - compileContext: anima-research#290 compiles at the live budget through compileAtLiveBudget, which settles a prepared budget change itself, in place of the trailing settleRuntimeSettingsTransition. Kept, with this branch's marksBefore taken before the compile and applyCompileWatermark in place of the plain watermark assignment. - compileWithInjections: anima-research#285 makes a dry run take no watermark and go straight to the compile, and anima-research#290 sends the rest through compileAtLiveBudget. Both kept, with marksBefore and applyCompileWatermark around them as above; a dry run's null watermark skips the apply. Every other file merged without a conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru
pushed a commit
that referenced
this pull request
Oct 14, 2026
…ontinuing-starts-keep-routing Main moved from 3f91700 through #281, #283, #278, #254, #316, #318, #322 and the 0.21.1 and 0.22.0 releases. Two conflicts, both resolved by keeping each side's intent: - src/framework.ts, beginAgentTurn's first lines: main added `const lifecycle = this.lifecycleGeneration;` (#281: a turn compiled before a stop() never drives after a restart) beside the line this branch renamed from `continuingTurn` to `resumesTurn`. Both are kept; no use of `continuingTurn` remains. - changelog.d/turn-routing-per-turn.fixed.md: the 0.22.0 release compiled #312's fragment into CHANGELOG.md and deleted it; this branch had edited it to drop "Each begins the sticky destination and hybrid's hold afresh, as before". The fragment stays deleted. The released entry is true of 0.22.0, and this branch's own fragment (continuing-starts-keep-routing.fixed.md) says what changes after it. What git merged on its own where both sides touch the same state: main's beginAgentTurn now drops a start before its stream sends anything when stop() has closed admission or ended the lifecycle the start began in (both before and after its compile), besides the agent's replacement it already dropped. Such a drop doesn't settle, so the open logical turn this branch adds is left as it is. The next commit makes a dropped start end the turn it was running. Main's other changes (the guard's withheld spills and their deadline, module events after stop(), trace listener copies, MCPL close info, workspace sync) touch no routing state.
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.
A module's event that arrives after
stop()began is now dropped with a log line naming the module, instead of throwingQueue is closed. This follows #263, which madeAgentFramework.pushEvent()throw afterstop()for a direct caller and drop a late tool result with a log line.Problem
A module's event after
stop()began.stop()closes the event queue first. It stops modules last, after the streams, the event loop and the typing indicators. In between, a module is still running:stop()can push a final event.On
main, both of a module's paths throwQueue is closed.ModuleContext.pushEvent()callsAgentFramework.pushEvent(), which rethrows for anything but a tool result, and the deprecatedModuleContext.queueis the raw queue. Thrown from a listener, it can go unhandled, as an uncaught exception or an unhandled rejection. Thrown from a module'sstop(), it makes the framework'sstop()reject withQueue is closed.Change
pushModuleEvent(), fromModuleContext.pushEvent()and from the deprecatedqueuehandle alike. Once the queue is closed, the event is dropped with a log line:[framework] a '<type>' event from module <name> arrived after stop() began — dropped. Nothing will process it, and from a listener a throw can go unhandled. Otherwise it's queued as before:pushEvent()tracesprocess:received, and thequeuehandle stays untraced.stop()began goes topushEvent()'s own drop, so its log line still names the call, the tool and its outcome.AgentFramework.pushEvent()still gets the throw for anything but a tool result, as fix(lifecycle): late tool results and MCPL arrivals at stop() settle instead of rejecting unhandled #263 decided. It can catch it, and learns the framework is stopped.ModuleRegistrytakes the pushing module's name as an optional second argument, in itsqueue(ModuleQueue) and itspushEventoption. AProcessQueueand a one-argument callback still fit.ModuleContext.queueis now an object withpushalone, as itsProcessQueuetype always said. It was the framework's own queue object.ModuleContext'spushEvent()andqueuedocs say what a push afterstop()began becomes.Not changed here
stop()leaves the event queue closed, as onmain. Opening it alone would let the old lifecycle's module contexts and late tool results into the new one, and the push coalescer and liveness watchdog stay stopped. start() after a completed stop() reuses an instance whose queue, coalescer and watchdog stay stopped #315 asks whether reusing an instance afterstop()is meant to be supported.stop()begins isn't waited for or fenced, as fix(lifecycle): late tool results and MCPL arrivals at stop() settle instead of rejecting unhandled #263's "Not changed here" says. That is a separate change.Tests
test/late-events-after-stop.test.ts, five tests:pushEvent()traced;stop()through both paths: each event is dropped with a line naming its module, andstop()completes;stop(), through both paths, logs fix(lifecycle): late tool results and MCPL arrivals at stop() settle instead of rejecting unhandled #263's line for the call;stop()finished: nothing throws, and each drop is logged;pushEvent()afterstop()still throws.On
main, the second, third and fourth fail: the second withstop()rejectingQueue is closed, the third where thequeuehandle's push throws.Each piece of the change fails one of these tests when it's removed: the drop, the untraced queue handle, the queue handle's route through the drop, the tool result's own line, and each module's name in its line.
Runs at
60636e03onmainc2f9baee(f1c6087fchanges only a comment and the changelog fragment), onenode --testprocess per file on Linux: Node 20.20.2, 1676/1676 across 133 files; Node 24.21.0, every file in its own process without--test-force-exit, 1676/1676.Basil-1007 (Claude Opus 5.5) and Daniel-2377 (GPT-6) reviewed it. Daniel-2377's probe showed that reopening the queue alone would let a stopped module's context into the next lifecycle, which is why the restart is in #315 rather than here.