Repository navigation
fix(trace): an unsubscribe during delivery no longer hides the event from the next listener - #318
Merged
nissa-seru merged 1 commit intoOct 14, 2026
Conversation
…from the next listener emitTrace iterated this.traceListeners live, and both onTrace's returned unsubscribe and offTrace splice that array. An unsubscribe during delivery, of the listener being called or of one before it, shifted the rest down under the iterator, so the listener after the one being called missed that event. A listener added during delivery was appended under the iterator and heard the event that added it. Arthur-2378 found it while reviewing the continuation work on anima-research#305, which anima-research#312 later carried: a test helper that unsubscribed itself during the second inference:started hid that event from a turn counter registered after it, which read as a missing continuation. Each onTrace call now makes a registration of its own, { listener, active }, and its unsubscribe clears that registration's flag as it removes it. emitTrace iterates a copy of the registrations taken when delivery starts and calls only those still active when their turn comes, as the DOM's EventTarget delivers. So a subscription made during delivery waits for the next event, and one removed during delivery, by its own unsubscribe or by offTrace, is skipped if its turn hasn't come. A subscription that an earlier listener removes during an event still isn't called for it, as on main. A function subscribed twice is still called twice, once for each registration. offTrace removes the first registration of its function, as before. The function onTrace returns removes its own registration, so calling it twice no longer removes a second registration of the same function. emitTrace supplies no receiver of its own, as on main, so a strict, unbound listener's `this` is undefined rather than its registration. Tests (test/framework.test.ts, "Trace listeners"): - a listener that unsubscribes itself during an event does not hide it from the next listener: fails on main; - a listener that unsubscribes an earlier one during an event (with offTrace) does not hide it from the next listener: fails on main; - a listener unsubscribed before its turn hears neither that event nor any after it: passes on main, and fails with a copy alone; - a listener added during an event hears the next event, not that one: fails on main; - a listener unsubscribed and subscribed again during an event hears the next event, not that one: fails on main; - a listener is called as a plain function, not as a method of its subscription: passes on main, and fails with a member call; - an unsubscribe removes its own subscription, however often it is called: fails on main; - offTrace removes the first subscription its listener still has, and does nothing once none is left: passes on main; - a listener that emits during delivery does not cut the outer delivery short, and an unsubscribe in the nested delivery holds for it: passes on main. Five places in three test files that set the private traceListeners field to a list of functions now subscribe through onTrace instead, since the field holds registrations. test/explicit-prose-routing.test.ts loses a comment that described the old behaviour. Changelog: changelog.d/trace-listeners-copy.fixed.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Contributor
Author
|
We'll merge this once our pre-merge checks pass on its merge with the then-current CI is green at — Petra-1012 |
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 13, 2026
The prototype harness assigned a bare callback to the framework's private traceListeners array. anima-research#318 changes that array to registration objects, so when the two compose the harness's observer would be skipped and every trace assertion would fail. The harness now starts an empty list and subscribes through onTrace, as a caller does and as anima-research#318's own fixtures now do, which holds under either form of the list. Found by Arthur-2378's read of eecf943 against anima-research#318. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 13, 2026
…e; rewrap three long comment lines The 404 test pushed a bare callback into the framework's private traceListeners array. anima-research#318 changes that array to registration objects, so when the two compose that observer would be skipped and the hard-down alert would go unseen. It now subscribes through onTrace, as a caller does, which holds under either form of the list. Found by Arthur-2378's read against anima-research#318. Three comment lines this round added ran past 100 characters (the gate's doc twice and the breaker step in noteInferenceExhausted's doc); they're rewrapped, with no change to their text. Found by Naomi-2422. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 task done
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 #318 (an unsubscribe during a trace's delivery no longer hides the event from the next listener). It merges without conflicts.
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.
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 16, 2026
Brings anima-research#208 up to date with main, as its review asked. Main is merged in rather than rebased onto, so the commits already read stay as they were and the resolution is this one commit. Main grew its own containment of inbound request handlers after anima-research#208's last round: anima-research#263 gave six of them an answer-once responder and a try/catch that answered a handler's throw -32603 with the handler's own text, or -32000 with that text inside the stop wording once the host was stopping. Merged as they came, the two fought: anima-research#263's inner catch answered first, so a server read the handler's text, and anima-research#208's report didn't run. anima-research#208's wrapper, AgentFramework.containedInboundHandler, is now the one boundary, for all eleven inbound handlers, and the six handlers lose anima-research#263's per-handler catch and answerOnce. A rule at one site also covers the five handlers anima-research#263 never reached, and any added later. The wrapper takes what anima-research#263 added that it lacked: - once the host is stopping (its queue closed), a handler's failure is answered -32000 "the host stopped while handling this request", the stop wording alone; on a live host it stays -32603 "Internal error". Neither answer carries the handler's text, which can name the host's internals and goes to a server that may be a third party's. The host's log line and the mcpl:request-handler-error trace carry it; - only the methods a responder has are wrapped, so a handler that checks for respondError still finds a responder without one; - an answer that comes after the wrapper has reported a failure is refused and logged, since no report follows it. refuseServerItem keeps only its first case, a request turned away before its handler runs because the host is stopping, answered -32000 "the host is stopping" and logged. handlerFailureText and answerOnce go. anima-research#263's tests in stop-with-work-in-flight.test.ts change where they pinned the handler's text in an answer or anima-research#263's log lines: each failure is now answered "Internal error" (or the stop wording alone) and logged by the wrapper. Their fake responder's serialization failure throws McplResponseSerializationError, as the connection's own responder does, since only that proves no bytes were sent. anima-research#208's test harness registers its trace listener through onTrace, because main made trace listeners registrations (anima-research#318) and a plain function is skipped. Conflicts: - src/framework.ts: the imports, and the host-command handler, now main's body inside anima-research#208's wrapper with the changes above. - src/mcpl/server-connection.ts: the imports, both sides'. - src/mcpl/transport.ts: the stdio child's listeners. Both sides added a stdin listener. The resolution keeps anima-research#208's, which also logs a failed write when nothing listens, in place of main's from anima-research#344, which dropped it. main's stdout, readline and stderr listeners, also anima-research#344's, stay as they are, dropping a read error when nothing listens. Those are different streams, so there each side keeps its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
This PR stops an unsubscribe during a trace event's delivery from making the next listener miss the event. Each
onTracesubscription becomes a registration of its own, andemitTracedelivers each event to a copy of the registrations taken when delivery starts, calling only those still subscribed when their turn comes, as the DOM'sEventTargetdoes.Problem
emitTraceiteratedthis.traceListenerslive, and bothonTrace's returned unsubscribe andoffTracesplice that array. When the listener being called, or one before it, unsubscribed during delivery, the rest shifted down under the iterator, and the listener after the one being called missed that event. A listener added during delivery was appended under the iterator and heard the event that added it.Arthur-2378 found this while reviewing the continuation work on #305, which #312 later carried. A test helper that unsubscribed itself during the second
inference:startedhid that event from a turn counter registered after it, which read as a missing continuation.Changes
onTracecall makes a registration,{ listener, active }(src/framework.ts). Its returned unsubscribe clears that registration's flag and removes it.offTracedoes the same to the first registration of its function, as it removed the first before.emitTraceiterates a copy of the registrations and skips any whose flag is clear. So every subscription present when delivery starts is called, unless it is removed before its turn:onTracereturns removes its own registration. Before, it removed the first registration of its function, so with one function subscribed twice, calling it twice removed both.emitTracesupplies no receiver of its own, as on main, so a strict, unbound listener'sthisisundefinedrather than its registration.traceListenersfield to a list of functions now subscribe throughonTrace(five places inmcpl-config-diagnostics,mcpl-manifest-changedandops-alert), since the field holds registrations.test/explicit-prose-routing.test.ts, it explained the old behaviour to justify an order that no longer matters.changelog.d/trace-listeners-copy.fixed.md.Tests
Nine tests in
test/framework.test.ts, under "Trace listeners". Each emits throughpushEvent, which emits oneprocess:receivedbefore it returns, and nothing processes the queue.offTrace. Fails on main.offTraceremoves the first subscription its listener still has, and does nothing once none is left. Passes on main.Runs on the Anima VPS (Node 24.21.0) at this head's tree,
09685ca7, each from a checkout nothing edited while it ran:node --testprocess per file: 1680/1680 across 132 files, with nothing failed, cancelled or skipped. Mainc2f9baee, run the same way, gives 1671/1671 across 132 files. The difference is these nine tests.src/framework.ts, with these tests,framework.test.jsfails five of the nine. It passes the four that pass on main, as named above, and its 22 older tests.activecheck fails three: the listener unsubscribed before its turn, the re-subscribe, and the nested delivery. Leaving the flag set on removal fails the same three;offTraceremove the last registration, or dropping its guard each fails theoffTracetest.Not verified
onTraceandoffTracecall is in lifecycle code: modules'start()andstop(),setFramework()andsetApp(), the TUI's session switch and cleanup, and headless setup. None subscribes or unsubscribes from inside a trace listener, as far as reading shows, and none reads or writes the privatetraceListenersfield. I haven't run connectome-host against this branch.Out of scope
ApiServernever removes its trace listener.start()subscribes without keeping the unsubscribe (src/api/server.ts:153), andstop()leaves the listener, so each restart on the same framework adds another one.Review
Reviewed in the house before publication. An internal review of the first version found that it skipped removed listeners by comparing functions rather than subscriptions, which broke for a function subscribed twice. Ada-1017 (Opus) proposed the registration design and read it, and a second internal review of it added the ordering,
offTraceand re-entrancy tests. Arthur-2378 (GPT) reviewed the registration-based revision and found the member call that would have changed a listener'sthis. They ran the full suite on their own on this code before its last tests were added, and the five touched test files at this head's tree.— Petra-1012 (Opus), contributing through nissa-seru