Repository navigation
fix(mcpl): a server closed for good leaves the registry, and a failed runtime connect settles its gate - #459
Merged
nissa-seru merged 6 commits intoOct 18, 2026
Conversation
added 6 commits
October 17, 2026 15:53
A reconnect: true server that the framework closes for good while it's down stayed registered. The registry drops an entry only from its 'close' listener or in closeServer (removeServer, closeAll), and close() emits no 'close' for a connection that's down between retries (the early return in McplServerConnection.close()). The framework closed servers for good with connection.close() at three sites: - connectMcplServerInternal's awareness-barrier catch, reached by a runtime connectMcplServer or restartMcplServer whose Discord awareness accounting fails. When the server's first connect had failed, what it closed was the stub connectWithReconnect returned, which stayed registered, so a later connectMcplServer of its id failed as already registered until disconnectMcplServer or restartMcplServer dropped it. Greptile's review of anima-research#451 found this path. - The "close" fallback in handleManifestChanged and in registerMcplServerFeatures, taken when a server refuses the host's policy. The server answered the policy request a moment before, so it's down there only if it dropped in between. Each site now calls the registry's closeServer, made public with a doc comment, which drops the entry once close() resolves, whatever the connection reports, if that connection is still the one registered. The awareness catch's comment now says why its close goes through the registry. Tests: a runtime connect whose accounting fails (recordFailure throwing) after the server's first connect failed, then a second connect of the id, which succeeds. The two fallback tests' harnesses hold their fake connection in a real registry, and the "close" fallback leaves it unregistered; the fakes' close() emits nothing, as a connection's does when it's down between retries.
…ts gate connectMcplServerInternal installs the data-plane gate before it awaits awareness accounting, which pauses every registered server's data plane. When accounting failed, the catch closed the new connection and rethrew, and never failed or completed that gate. Every other runtime gate calls failMcplDataPlaneGate when its accounting fails: quiesce resume(), explicit syncDiscordAwarenessMarkers(), tools-list reconciliation and reconnect reconciliation. So after a runtime connectMcplServer or restartMcplServer whose accounting failed, every other server's pushes, incoming messages and inference requests were held until some other gate completed. A probe without this commit's change ran a server, then a runtime connect whose accounting failed: the failed connect's gate stayed current, and the running server's data plane stayed paused, still 3 s later. The catch now calls failMcplDataPlaneGate after the close for good, as those paths do. While serving, that recycles the running servers: each with reconnect: true comes back, and its data flows once its reconnect's accounting succeeds, while a server without it stays closed, as on those paths. While stopping, it releases the gate. The close comes first, so the connection being refused is out of the registry and isn't recycled, unless its close fails, which keeps its entry for a later close to retry. Tests: the runtime-connect fixture moves into a helper that stops the framework if anything after create() fails. A new test runs a reconnecting server, fails a runtime connect's accounting, and checks that the gate failed for the running server (its "accounting unhealthy" mcpl:server-error trace) and that the recycled server's next startup push is answered.
…r the order, the identity guard and the closed server Answers the internal review of the first two commits. A runtime connect whose policy registration throws after its awareness accounting succeeded left the new connection live and registered, and the gate it installed standing over every other server's data planes, since only the success path completed the gate. That catch now closes the new connection for good through the registry, as the accounting catch does, and completes the gate rather than failing it: accounting succeeded, so nothing calls for a recycle. Both catches close through closeFailedRuntimeConnect, which logs a close that fails instead of discarding it. Such a close keeps the entry, so a later disconnect, restart or shutdown can close it again. close()'s comment now says only closeAll bounds its caller's wait for an in-flight close. Tests: - A runtime connect whose policy registration throws (initializeServer made to throw for it) while another server runs: the connect rejects, the gate is completed, the running server stays connected with its data plane ready and no gate fails, and the id connects again. - The runtime-connect helper checks that the refused connection gets no "accounting unhealthy" trace, which it would if the gate failed before the close took it out of the registry. - The stub test's running server has no reconnect: true; it checks that the server ends neither connected nor retrying. - A registry test: closeServer leaves an entry that a newer connection holds under the same id, as after a restart. The changelog fragment covers the policy registration exit and a failed close.
…sed; test a close that fails Answers the internal review of the previous commit. When a runtime connect's policy registration throws and the close that follows fails, the connection stays registered, as closeServer keeps it. Completing the gate then ready()d it along with every other connection. ready() doesn't check whether a connection is closed, so what it held that needs no grant would have reached listeners. completeMcplDataPlaneGate now takes a connection to leave paused, and the policy exit passes the one it is closing. Tests: the policy-registration test becomes a helper that runs twice. With a close that rejects once, the connect still rejects with the registration's error, the connection stays registered and paused, the gate is completed, and the failed close is logged.
…the completion's skip Answers the review of the previous commit. The previous commit left the connection a failed connect closes paused at that connect's own completion, but a later gate's completion, or a release of control traffic, still readied it: ready() and readyControlPlane() didn't check whether the connection was closed. The same held for any connection a failed close keeps registered, on main too. ready() and readyControlPlane() now return at once when the connection is closed for good: closed, with reconnect off. close() turns reconnect off and marks the connection closed before its transport close can fail, and a drop with reconnect off leaves it the same way. A connection between retries keeps reconnect on, so it still readies after its reconnect. completeMcplDataPlaneGate's skip goes. While the host stops, a connection closed for good, such as one closeAll has begun closing, no longer passes what it held to the stopping refusal, whose answers can't be sent on a closed connection anyway. What every other connection held reaches it as before. Tests: the failed-close run now fails the transport close, after close() has marked the connection, and checks that a later syncDiscordAwarenessMarkers() leaves it paused. A stop-path test holds a control event and a data event on two connections, closes one, and checks that releasing and then failing the gate while the host stops lets out only the other one's.
…a close still under way Answers the internal review of the previous commit. ready()'s comment gave the reason for doing nothing once a connection is closed for good as its server learning of the close from the close itself, which a close that fails doesn't give. The reason that holds is that nothing it held could be answered: a closed connection sends no response, and its reset policy refuses what needs a grant. Readying it would only pass what needs no grant to listeners of a server that won't be served again. The comment now says so, and that a connection between retries still readies, so a reconnect stub's connect-failed reaches its listeners. failMcplDataPlaneGate's comment no longer says everything the pause held back reaches the stopping refusal. The changelog fragment states the stopping case for every connection closed for good, not only one being closed, and no longer says the running servers' data flows once a gate completes, which a quiesced host doesn't do. Tests: the stop-path test starts closing a connection without waiting for the close, so the gate is released and then failed while that close is still under way, the state a connection closeAll has begun closing is in.
[Medium impact] The PR appears safe to merge; no actionable regression was found. SummaryFixes cleanup after failed runtime MCPL connects and policy refusals.
Acknowledged by Review used code and test inspection. Tests were not run. Reviews (1) · Last reviewed commit: "fix(mcpl): say what a connection closed ..." · Reviewed by Greptile |
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 18, 2026
main gained anima-research#457, anima-research#458, anima-research#459, anima-research#454, anima-research#460, anima-research#464, anima-research#412 and anima-research#467 since 86a3683. One block conflicts, in restorePersistedDeferredWrites: anima-research#454 narrowed its try to reading the recovery file, so that a file it can't read or take fails create(), and moved the parsing and numbering after it, while this branch changed that numbering (boot numbers the restored queue 1..N). The resolution takes main's side of the block, then makes this branch's change to main's numbering where it now stands: the queue is ordered by valid seq, then the rest in file order, numbered 1..N after the already-landed entries are left out, and the counter goes on from N; the file is rewritten for an entry without a valid seq, not for the new numbers. main's check for writes that already landed, which now refuses create() when a context can't be read, is kept as main has it, over the ordered entries. Everything else merged without a conflict. The only other change of main's within forty lines of one of this branch's is anima-research#457's to an ephemeral run's idle watchdog, which now rejects a run that stop() has begun to end as stopped; it sits above the run's finally, which this branch changes, and neither depends on the other. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 18, 2026
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 18, 2026
main brings agent-framework anima-research#456 (the workspace file tools' fixes), anima-research#460 (editors' files and nested mounts), anima-research#410, anima-research#455, anima-research#457, anima-research#458, anima-research#459, anima-research#454, anima-research#464, anima-research#412 and anima-research#467. - write, edit and delete: anima-research#456 answers a change stored in the workspace but not on disk with one frame (storedButUnfinished), which leads with "stored, so don't send it again" and gives the push's reason. This branch had reworded those frames to say where force goes. Its frame goes and anima-research#456's stays: every reason a write's push can return names materialize where it offers force, and so do this branch's mode notes. Each success keeps its modeKept note, and commitToolChange keeps anima-research#456's onCommitted and answers { refused, modeKept }. - write: an overwrite keeps the entry's mode inside anima-research#456's guarded commit, as an edit does. - reconcile.ts: an effect that fails untouched names its kind, a chmod step's included, beside anima-research#456's account of what's in the way (inTheWay). - edit: anima-research#456's edit takes a change on disk in first, with a pass over its path. Where disk changed only a file's mode, that pass takes the mode in, as a listing does, and the edit's note read it as disk's copy having changed and the edit applying to that text. A mode taken in alone isn't a change to the text, so the note no longer counts one. The edit then writes at the mode disk has and leaves nothing owed. - anima-research#460: a push skips a path in another mount's directory before it plans, so it plans no chmod there either. - The changelog fragment drops its sentence about the write tools' frame, which anima-research#456's replaces, and no longer names an edit among what says that nothing records which side changed a mode after a refused widening, since an edit's pass takes disk's mode in first. - Tests: the test of a refused narrowing's frame takes anima-research#456's wording, the test of the force sentence goes with that sentence, and two tests pin an edit after a mode disk changed and after a refused widening. 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 18, 2026
…'s close of a failed runtime connect, and train fix-c and after Conflicts, and what each keeps: - settlePolicyExchange's tail: this branch's. main's side there is the barrier's release, completion and failure (installMcplDataPlaneGate and the rest), which this branch removes; anima-research#459 changed only a comment in it. - connectMcplServerInternal's tail: this branch's, with no data-plane gate. main's side is the gate's install, its accounting wait and its completion, with anima-research#459's handling of a failed step inside it. - test/mcpl-awareness-barrier.test.ts stays deleted, with the barrier it tested. Carried from main (anima-research#459, R2-1 of the maintainer's review of 3fb0415): - A runtime connect or restart whose policy registration throws closes the new connection for good through the registry (closeFailedRuntimeConnect), then rethrows, so it isn't left connected and half registered, and a later connect of its id isn't refused as already registered. Here that wraps the connect's settlePolicyExchange for a connect that isn't staged, as main's close is. A staged startup connect's failure goes to initializeMcpl's loop, which logs it and goes on with the other servers, on main as here. anima-research#459's other failed step, awareness accounting, has no counterpart here: no connect accounts for awareness. - anima-research#459's tests that don't need the barrier move to mcpl-reconnect-registration: a runtime connect whose policy registration throws closes its connection for good and a later connect of its id reaches the server; one whose close then fails stays registered and paused through a resume, and the close is logged; and a connection closed for good readies nothing, while a running one lets out what it held. Its tests of a failed accounting and of a failed gate test the barrier, and go with it. anima-research#459's server-connection and registry changes, a refused policy's close fallback through the registry, and the rest of main merge without a conflict. In src/framework.ts, anima-research#454 meets this branch in initializeMcpl, where it reads the push coalescer's and checkpoints' state before anything is built, and anima-research#457 in stopOwned, where it ends ephemeral runs after the loop; neither touches what this branch changes there. 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 18, 2026
…ts marker A runtime connect or restart whose policy registration throws closes the new connection for good, through the registry, as anima-research#459 does on main. The close ran in connectMcplServerInternal's catch, after settlePolicyExchange's finally had lifted the exchange marker, and the registry closes a connection in a later microtask. So for at least two microtask turns the failing connection was connected, unmarked and not yet closed, and an opener that ran then, such as a resume's readyMcplPlanes finishing meanwhile, would ready it and let out what it held with no grant. On main the window is wider: a resume's own gate readies every registered server, a runtime connect's included, at any point in its exchange. This branch's marker narrowed it to the failure edge, and this closes the edge. settlePolicyExchange takes closeOnFailure, which a runtime connect or restart passes: an exchange that throws closes the connection in the exchange's own catch, before its finally lifts the marker. A close that fails still marks the connection closed for good, so it readies nothing. The wrap in connectMcplServerInternal goes, and with it a comment that said a staged startup connect's failure ends startup: initializeMcpl's loop logs it and goes on with the other servers, on main as here. The comment now says so. The policy-failure test records, as the failing connection's close begins, which marks it closed for good, whether its exchange marker still holds it, and asserts that it does. 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 18, 2026
Brings in main since 86a3683, among it: one MCPL tool resolver (anima-research#412), a runtime connect that settles its data-plane gate on every exit, and the framework's closes for good going through the registry (anima-research#459), and a forced test run that no longer loses a test file's output (anima-research#464). Conflicts, resolved by hand: - src/framework.ts, executeMcplToolCall: anima-research#412's signature, the server id and the prefix the resolver matched, with this branch's modern-server branch looked up by that id. The error's redaction reads the server's configuration by the id, as the tool policy check does. - src/mcpl/server-connection.ts: - holdsUnreapedChild beside anima-research#459's closedForGood. - close() keeps this branch's closeStep. anima-research#459 changed only a comment in the code closeStep replaced. - test/mcpl-config-diagnostics.test.ts: this branch's two redaction tests, then anima-research#459's renamed test of a refused initial policy. Beyond the conflicts: test/mcp-recorded-text.test.ts calls executeMcplToolCall with anima-research#412's signature, the server's id and prefix. Merged without a conflict, and read against this branch: - dropMcplToolClasses takes anima-research#412's drop by the listing's owner, which both engines' close handlers call. - connectMcplServerInternal takes anima-research#459's settling of the gate: after the gate's install, an accounting failure closes the new connection through the registry and fails the gate, a policy registration that throws closes it and completes the gate, and success completes it. Nothing else returns or throws after the install. - The registry's closeServer, now public, drops an entry only once its connection's close() resolves, so a connection whose close can't reap its child stays registered, holding its id. Checks, on Linux, Node 24.22.0, at this commit's tree: the typecheck and the build pass, and 19 files of the merged code's tests pass, each run on its own and unforced: mcpl-config-diagnostics 14, mcpl-awareness-barrier 34, mcpl-manifest-changed 9, shutdown-errors 29, mcp-server-ownership 37, mcp-modern-framework 18, mcp-modern-outcomes 38, mcpl-legacy-lifetime 16, mcpl-runtime-lifecycle 37, mcpl-tool-resolution 13, mcpl-tool-classes 9, mcpl-tool-class-listing 4, mcpl-tool-lifecycle 31, mcp-recorded-text 10, stop-with-host-operations 58, failed-create-releases 11, checkpoint-attribution 10, present-while-acting 57 and script-structured-references 8. 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 18, 2026
…s failure reaches its listeners, and nothing is written after a pipe fails A review of the round that answers R2-1 and merges main e670363. Nothing more goes to a child whose pipe failed: - StdioTransport.writeLine writes nothing once a pipe has failed. Only the request paths checked pipeFailure, so after a stdout-only failure a legacy notification or response still reached the child, though the getter's doc said no write does. Every write passes writeLine, so the rule sits there; the engines' request checks stay, so a request is refused as not sent. The getter's doc and both refusal comments now give that reason: the transport is ending the child. - The first pipe error starts the one attempt to end the child, and a later one, from either pipe, adds nothing. Each used to attach its own catch, so one failed attempt could say 'unreaped' twice. A close for good that fails: - A legacy close() that can't reap its child emits no 'close', so no close listener takes the server's tools off the list. A disconnect and a recycle refresh the list on such a failure themselves. A failed runtime connect's close (main's anima-research#459) only logged it, and the initial and post-manifest policy refusals' 'close' fallbacks closed inside the policy request's try, so a failed close was logged as an unanswered policy, and on a manifest change the server's grant and listing stood. - closeMcplServerForGood closes a legacy connection for good through the registry, and when the close fails, refreshes the tool list and rethrows. The failed runtime connect's close and both fallbacks use it, the fallbacks after the policy try, so a failed close is reported as itself. A held launch's permanent failure: - reconnect-failed is control-plane, so it's held until the first ready(). main's anima-research#459 has ready() and readyControlPlane() do nothing for a connection closed for good, which a launch held as unreaped is, so a loss held before the first gate released it never reported its permanent failure: no mcpl-down alert at startup, and no refresh. Both now deliver a closed-for-good connection's own buffered connect-failed and reconnect-failed, which need no grant, and nothing else, so anima-research#459's rule still holds for what the server sent. A connect that stop() ends, whose cleanup can't reap: - Its error offered "disconnect it to retry the reap", which stop() refuses once it has begun, and the legacy one left out that the framework had stopped. In both engines it now says the framework stopped while it was connecting and its cleanup failed, and offers no disconnect. McplUnreapedLaunchError keeps its cleanup's failure (cleanup) for that. Text: - During a connect, a child whose pipe failed and that outlives SIGKILL is held when the connect fails, by the connect's own cleanup, not at once: the modern engine has no session to lose yet, and the legacy handshake doesn't listen for 'unreaped'. The comments at both wirings and the changelog fragment say so. Tests: the recycle guard's reconnect side (a recycle whose close can't reap leaves the tools listed for the reconnect loop), a close() on a failed attempt while the child runs (rejects again, no signal), the legacy written request's outcome (no-response), close() arriving while the child runs, and a test name that promised a 2 s it didn't measure. Red-first, Linux, Node 24.22.0: - With src at the commit before, 7 new tests fail: in mcpl-transport-close, the write after a pipe failure and both early holds (ready() and readyControlPlane()); in mcp-server-ownership, both engines' stop-ended connects; in mcpl-config-diagnostics, the failed fallback close; in mcpl-awareness-barrier, the failed runtime connect's close. The recycle guard's new test passes there, since the guard was already in the code; its removal below fails it. The tightened assertions in older tests pin behaviour that was already right. - Each of 12 pieces, removed on its own, fails a test: the write gate, the first-error rule, the helper's refresh, the runtime connect's close through the helper, each fallback's close after the policy try, delivering the connection's own reports from ready() and from readyControlPlane(), delivering only those, the stop-aware advice, the legacy stop text (whose removal also fails the typecheck), and the recycle guard's condition. - With the change, these files pass, each run on its own and unforced: mcpl-transport-close 22, mcp-modern-outcomes 40, mcp-server-ownership 40, mcpl-config-diagnostics 15 and mcpl-awareness-barrier 34 on Node 24.22.0 and 20.20.2, and mcpl-legacy-lifetime 16, mcpl-runtime-lifecycle 37, stop-with-host-operations 58, mcpl-manifest-changed 9, failed-create-releases 11, mcp-modern-framework 18 and mcp-recorded-text 10 on Node 24.22.0. 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 19, 2026
…t pass, item 8) Ada's call on the rent pass: the MCPL reconnect fixes leave anima-research#250 for a pull request of their own on main, since main has the defect too (Esther-2584's J20464: main refuses a registration sent right after a reconnect's initialize, -32002), so the fix reaches every host's next release whether or not anima-research#250 is ready. Crispin-2713 carries it. The line between it and what anima-research#250 keeps was drawn with him in room-203: the exchange marker goes with the fixes, since main's own gates open a connection mid-exchange too, and without it anima-research#250 opens planes mid-exchange only where main can (its two openers of every connection, startup and resume(), are among main's five). What anima-research#250 keeps for its gate's removal (item 3): - readyMcplPlanes as the one opener, with the quiesce check only: no marker and no isConnected skip, so it readies a disconnected stub as main's gate does, and the stub's buffered connect-failed comes out at startup's open; - a connect runs its exchange, then opens its own planes; a runtime connect or restart whose policy registration throws closes the connection for good in the connect's own catch, as main does since anima-research#459; - the reconnect handler runs its exchange, then opens its connection's planes. Out of anima-research#250, to the reconnect pull request: settlePolicyExchange and mcplPolicyExchanges, the transport-epoch checks (in the reconnect handler, the connect and registerMcplServerFeatures), every change to server-connection.ts but four comments (control traffic held across a reconnect, the transport boundary's keep and drop rules, the flush order, the responder's epoch binding, replaceHeld and the held-request count across a boundary, the handshake's close), their tests in mcpl-reconnect-events and mcpl-reconnect-registration, and mcpl-reconnect-registration.fixed.md. - server-connection.ts is main's (9e3e407) but for the comments that named the awareness gate, which this branch removes; they now say what holds the data plane without it. - mcpl-reconnect-events.test.ts is main's again. - anima-research#459's tests stay, since main's sit in the awareness-gate test file this branch deletes: they move to mcpl-runtime-connect-failure.test.ts, without the check that the marker held the connection at its close. The fixture they and failed-create-releases use, reregistering-mcpl-server.mjs, stays as it is. - mcpl-reconnect-reregistration's harness loses the marker and the connection's epoch fields. 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 19, 2026
…anima-research#506's words and F240 main moved 331 commits on from 9e3e407, among them anima-research#262 (the modern MCP client and the legacy engine's protocol boundary), anima-research#506 (the agent's words for failed requests), anima-research#532, anima-research#537, anima-research#210 and anima-research#542 (F240: a redaction that can't read its configuration withholds the text). What anima-research#250 removes stays removed, and what main added keeps working beside it. Conflicts: - src/framework.ts, imports: main's new imports, with anima-research#250's McplRequestError beside McplProtocolVersionError and McplUnreapedLaunchError. - src/framework.ts, the barrier: main changed failMcplDataPlaneGate's recycle (a connection it couldn't close takes its tools off the list). anima-research#250 removes the gate and its recycle, so that change has nowhere to go; the close for good it rests on (closeMcplServerForGood) is main's and stays, and a runtime connect whose policy registration throws still goes through it. - src/framework.ts, the connect: main split connectMcplServerInternal into the id hold and connectHeldMcplServer, with a modern family, a stop check and an abort. That structure is main's. anima-research#250's parts go into it: the parameter is `staged`, the ledger is read first only by a legacy connect that derives the protective default (anima-research#542's sentence about a ledger that breaks after that read stays), listeners are wired before any plane opens, a runtime connect whose policy registration throws closes for good, and a staged startup connect returns its connection while a runtime one opens its planes and starts delivery in the background before returning it. - src/framework.ts, the restart: main's restart (its in-flight, claim, id and prefix checks, and its claims) with anima-research#250's rule for the ledger read: only a legacy server that derives the default reads it before disconnecting. - src/mcpl/server-connection.ts: main's file, which moved the reconnect into reconnectOnce, with anima-research#250's five rewrites of comments that named the awareness barrier. - test/failed-create-releases: main's two-engine test, with anima-research#250's legacy server (`chat`, the reregistering fixture) and no ledger options. - test/mcpl-reconnect-reregistration, test/stop-with-host-operations: wireMcplEvents takes the config now. - test/stop-ends-awareness-drains: anima-research#250's assertions, with anima-research#506's words for a request the close ended. - test/placed-reaction-baseline: anima-research#542's two tests go in ahead of anima-research#250's restart tests. The reconnect one breaks the ledger with breakLedger, since version 3 reads the file only when it has no document in memory. - test/mcpl-awareness-barrier.test.ts and its fixture stay deleted. Of main's changes to them, the deadline wording and the generation tests' signatures go with the file; the relist after a failed close is anima-research#459's test, which anima-research#250 keeps in mcpl-runtime-connect-failure.test.ts, so that assertion moves there. Outside the conflicts: - test/fixtures/reregistering-mcpl-server.mjs, anima-research#250's own fixture, answers initialize with protocolVersion 2024-11-05: main's legacy engine refuses an initialize answer without it (anima-research#262), and main gave its own fixtures the field. - test/discord-awareness-delivery's inbound test passes a config to wireMcplEvents, which records through that server's redactor now. - main's modern-MCP connect and restart read no ledger and install no gate, so they meet nothing anima-research#250 removed. Two of main's comments, and the modern client's pending fragment, named an awareness gate as MCPL surface a modern server lacks; with no gate left, they no longer name it. - main's modern test in placed-reaction-baseline breaks the ledger with breakLedger too, for the same reason. - anima-research#506's words reach anima-research#250's own assertions: a hung reaction's recorded error says the server didn't answer within its deadline, and a request the connection's loss ended says the connection closed before it answered. anima-research#250's test of a request the host's close ends goes: main's "closing the connection while a request awaits its answer" pins the same outcome, and stop-ends-awareness-drains pins its words. - test/mcp-server-ownership: main's two tests of the gate's recycle (an accounting failure recycling a server whose child can't be reaped, with and without reconnect) call installMcplDataPlaneGate and failMcplDataPlaneGate, which anima-research#250 removes, so they go with the gate. The close for good they lead to is still pinned for a runtime connect's failure, in mcpl-runtime-connect-failure. 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 19, 2026
The MCPL reconnect fixes leave anima-research#250 for a pull request of their own on main, since main has the defect too (main refuses a registration sent right after a reconnect's initialize, -32002), so the fix reaches every host's next release whether or not anima-research#250 is ready. The line between that pull request and what anima-research#250 keeps: the exchange marker goes with the fixes, since main's own gates open a connection mid-exchange too, and without it anima-research#250 opens planes mid-exchange only where main can (its two openers of every connection, startup and resume(), are among main's five). What anima-research#250 keeps for its gate's removal: - readyMcplPlanes as the one opener, with the quiesce check only: no marker and no isConnected skip, so it readies a disconnected stub as main's gate does, and the stub's buffered connect-failed comes out at startup's open; - a connect runs its exchange, then opens its own planes; a runtime connect or restart whose policy registration throws closes the connection for good in the connect's own catch, as main does since anima-research#459; - the reconnect handler runs its exchange, then opens its connection's planes. Out of anima-research#250, to the reconnect pull request: settlePolicyExchange and mcplPolicyExchanges, the transport-epoch checks (in the reconnect handler, the connect and registerMcplServerFeatures), every change to server-connection.ts but four comments (control traffic held across a reconnect, the transport boundary's keep and drop rules, the flush order, the responder's epoch binding, replaceHeld and the held-request count across a boundary, the handshake's close), their tests in mcpl-reconnect-events and mcpl-reconnect-registration, and mcpl-reconnect-registration.fixed.md. - server-connection.ts is main's (9e3e407) but for the comments that named the awareness gate, which this branch removes; they now say what holds the data plane without it. - mcpl-reconnect-events.test.ts is main's again. - anima-research#459's tests stay, since main's sit in the awareness-gate test file this branch deletes: they move to mcpl-runtime-connect-failure.test.ts, without the check that the marker held the connection at its close. The fixture they and failed-create-releases use, reregistering-mcpl-server.mjs, stays as it is. - mcpl-reconnect-reregistration's harness loses the marker and the connection's epoch fields. 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 19, 2026
…arch#506's words and anima-research#542 main moved 331 commits on from 9e3e407, among them anima-research#262 (the modern MCP client and the legacy engine's protocol boundary), anima-research#506 (the agent's words for failed requests), anima-research#532, anima-research#537, anima-research#210 and anima-research#542 (a redaction that can't read its configuration withholds the text). What anima-research#250 removes stays removed, and what main added keeps working beside it. Conflicts: - src/framework.ts, imports: main's new imports, with anima-research#250's McplRequestError beside McplProtocolVersionError and McplUnreapedLaunchError. - src/framework.ts, the barrier: main changed failMcplDataPlaneGate's recycle (a connection it couldn't close takes its tools off the list). anima-research#250 removes the gate and its recycle, so that change has nowhere to go; the close for good it rests on (closeMcplServerForGood) is main's and stays, and a runtime connect whose policy registration throws still goes through it. - src/framework.ts, the connect: main split connectMcplServerInternal into the id hold and connectHeldMcplServer, with a modern family, a stop check and an abort. That structure is main's. anima-research#250's parts go into it: the parameter is `staged`, the ledger is read first only by a legacy connect that derives the protective default (anima-research#542's sentence about a ledger that breaks after that read stays), listeners are wired before any plane opens, a runtime connect whose policy registration throws closes for good, and a staged startup connect returns its connection while a runtime one opens its planes and starts delivery in the background before returning it. - src/framework.ts, the restart: main's restart (its in-flight, claim, id and prefix checks, and its claims) with anima-research#250's rule for the ledger read: only a legacy server that derives the default reads it before disconnecting. - src/mcpl/server-connection.ts: main's file, which moved the reconnect into reconnectOnce, with anima-research#250's five rewrites of comments that named the awareness barrier. - test/failed-create-releases: main's two-engine test, with anima-research#250's legacy server (`chat`, the reregistering fixture) and no ledger options. - test/mcpl-reconnect-reregistration, test/stop-with-host-operations: wireMcplEvents takes the config now. - test/stop-ends-awareness-drains: anima-research#250's assertions, with anima-research#506's words for a request the close ended. - test/placed-reaction-baseline: anima-research#542's two tests go in ahead of anima-research#250's restart tests. The reconnect one breaks the ledger with breakLedger, since version 3 reads the file only when it has no document in memory. - test/mcpl-awareness-barrier.test.ts and its fixture stay deleted. Of main's changes to them, the deadline wording and the generation tests' signatures go with the file; the relist after a failed close is anima-research#459's test, which anima-research#250 keeps in mcpl-runtime-connect-failure.test.ts, so that assertion moves there. Outside the conflicts: - test/fixtures/reregistering-mcpl-server.mjs, anima-research#250's own fixture, answers initialize with protocolVersion 2024-11-05: main's legacy engine refuses an initialize answer without it (anima-research#262), and main gave its own fixtures the field. - test/discord-awareness-delivery's inbound test passes a config to wireMcplEvents, which records through that server's redactor now. - main's modern-MCP connect and restart read no ledger and install no gate, so they meet nothing anima-research#250 removed. Two of main's comments, and the modern client's pending fragment, named an awareness gate as MCPL surface a modern server lacks; with no gate left, they no longer name it. - main's modern test in placed-reaction-baseline breaks the ledger with breakLedger too, for the same reason. - anima-research#506's words reach anima-research#250's own assertions: a hung reaction's recorded error says the server didn't answer within its deadline, and a request the connection's loss ended says the connection closed before it answered. anima-research#250's test of a request the host's close ends goes: main's "closing the connection while a request awaits its answer" pins the same outcome, and stop-ends-awareness-drains pins its words. - test/mcp-server-ownership: main's two tests of the gate's recycle (an accounting failure recycling a server whose child can't be reaped, with and without reconnect) call installMcplDataPlaneGate and failMcplDataPlaneGate, which anima-research#250 removes, so they go with the gate. The close for good they lead to is still pinned for a runtime connect's failure, in mcpl-runtime-connect-failure. 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 19, 2026
The MCPL reconnect fixes come out of anima-research#250: main has the defect too (main refuses a registration sent right after a reconnect's initialize, -32002), so they are a change to main of their own, anima-research#544. The line between the fixes and what anima-research#250 keeps: the exchange marker goes with the fixes, since main's own gates open a connection mid-exchange too, and without it anima-research#250 opens planes mid-exchange only where main can (its two openers of every connection, startup and resume(), are among main's five). What anima-research#250 keeps for its gate's removal: - readyMcplPlanes as the one opener, with the quiesce check only: no marker and no isConnected skip, so it readies a disconnected stub as main's gate does, and the stub's buffered connect-failed comes out at startup's open; - a connect runs its exchange, then opens its own planes; a runtime connect or restart whose policy registration throws closes the connection for good in the connect's own catch, as main does since anima-research#459; - the reconnect handler runs its exchange, then opens its connection's planes. Out of anima-research#250, with the fixes: settlePolicyExchange and mcplPolicyExchanges, the transport-epoch checks (in the reconnect handler, the connect and registerMcplServerFeatures), every change to server-connection.ts but four comments (control traffic held across a reconnect, the transport boundary's keep and drop rules, the flush order, the responder's epoch binding, replaceHeld and the held-request count across a boundary, the handshake's close), their tests in mcpl-reconnect-events and mcpl-reconnect-registration, and mcpl-reconnect-registration.fixed.md. - server-connection.ts is main's (9e3e407) but for the comments that named the awareness gate, which this branch removes; they now say what holds the data plane without it. - mcpl-reconnect-events.test.ts is main's again. - anima-research#459's tests stay, since main's sit in the awareness-gate test file this branch deletes: they move to mcpl-runtime-connect-failure.test.ts, without the check that the marker held the connection at its close. The fixture they and failed-create-releases use, reregistering-mcpl-server.mjs, stays as it is. - mcpl-reconnect-reregistration's harness loses the marker and the connection's epoch fields. 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 19, 2026
…arch#506's words and anima-research#542 main moved 331 commits on from 9e3e407, among them anima-research#262 (the modern MCP client and the legacy engine's protocol boundary), anima-research#506 (the agent's words for failed requests), anima-research#532, anima-research#537, anima-research#210 and anima-research#542 (a redaction that can't read its configuration withholds the text). What anima-research#250 removes stays removed, and what main added keeps working beside it. Conflicts: - src/framework.ts, imports: main's new imports, with anima-research#250's McplRequestError beside McplProtocolVersionError and McplUnreapedLaunchError. - src/framework.ts, the barrier: main changed failMcplDataPlaneGate's recycle (a connection it couldn't close takes its tools off the list). anima-research#250 removes the gate and its recycle, so that change has nowhere to go; the close for good it rests on (closeMcplServerForGood) is main's and stays, and a runtime connect whose policy registration throws still goes through it. - src/framework.ts, the connect: main split connectMcplServerInternal into the id hold and connectHeldMcplServer, with a modern family, a stop check and an abort. That structure is main's. anima-research#250's parts go into it: the parameter is `staged`, the ledger is read first only by a legacy connect that derives the protective default (anima-research#542's sentence about a ledger that breaks after that read stays), listeners are wired before any plane opens, a runtime connect whose policy registration throws closes for good, and a staged startup connect returns its connection while a runtime one opens its planes and starts delivery in the background before returning it. - src/framework.ts, the restart: main's restart (its in-flight, claim, id and prefix checks, and its claims) with anima-research#250's rule for the ledger read: only a legacy server that derives the default reads it before disconnecting. - src/mcpl/server-connection.ts: main's file, which moved the reconnect into reconnectOnce, with anima-research#250's five rewrites of comments that named the awareness barrier. - test/failed-create-releases: main's two-engine test, with anima-research#250's legacy server (`chat`, the reregistering fixture) and no ledger options. - test/mcpl-reconnect-reregistration, test/stop-with-host-operations: wireMcplEvents takes the config now. - test/stop-ends-awareness-drains: anima-research#250's assertions, with anima-research#506's words for a request the close ended. - test/placed-reaction-baseline: anima-research#542's two tests go in ahead of anima-research#250's restart tests. The reconnect one breaks the ledger with breakLedger, since version 3 reads the file only when it has no document in memory. - test/mcpl-awareness-barrier.test.ts and its fixture stay deleted. Of main's changes to them, the deadline wording and the generation tests' signatures go with the file; the relist after a failed close is anima-research#459's test, which anima-research#250 keeps in mcpl-runtime-connect-failure.test.ts, so that assertion moves there. Outside the conflicts: - test/fixtures/reregistering-mcpl-server.mjs, anima-research#250's own fixture, answers initialize with protocolVersion 2024-11-05: main's legacy engine refuses an initialize answer without it (anima-research#262), and main gave its own fixtures the field. - test/discord-awareness-delivery's inbound test passes a config to wireMcplEvents, which records through that server's redactor now. - main's modern-MCP connect and restart read no ledger and install no gate, so they meet nothing anima-research#250 removed. Two of main's comments, and the modern client's pending fragment, named an awareness gate as MCPL surface a modern server lacks; with no gate left, they no longer name it. - main's modern test in placed-reaction-baseline breaks the ledger with breakLedger too, for the same reason. - anima-research#506's words reach anima-research#250's own assertions: a hung reaction's recorded error says the server didn't answer within its deadline, and a request the connection's loss ended says the connection closed before it answered. anima-research#250's test of a request the host's close ends goes: main's "closing the connection while a request awaits its answer" pins the same outcome, and stop-ends-awareness-drains pins its words. - test/mcp-server-ownership: main's two tests of the gate's recycle (an accounting failure recycling a server whose child can't be reaped, with and without reconnect) call installMcplDataPlaneGate and failMcplDataPlaneGate, which anima-research#250 removes, so they go with the gate. The close for good they lead to is still pinned for a runtime connect's failure, in mcpl-runtime-connect-failure. 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.
Fixes for a runtime MCPL connect that fails partway, and for servers the framework closes for good. A
reconnect: trueserver that the framework closes for good now leaves the registry even when it's down at the time, so a later connect of its id isn't refused as already registered. A runtime connect now settles the data-plane gate it installed on every exit, so when its awareness accounting or its policy registration fails, the other MCPL servers' data is no longer held behind that gate. And a connection closed for good no longer lets out what it held.Problem
On
main(3af13bf4):A server closed for good while it's down stays registered. The registry drops an entry from its
'close'listener, for a connection that won't reconnect (src/mcpl/server-registry.ts:53-55), or incloseServer, whichremoveServerandcloseAllcall (:72,:144,:151).McplServerConnection.close()returns without emitting'close'when the connection is already closed, with no close left to finish and no reconnect attempt in flight (src/mcpl/server-connection.ts:939). A reconnect stub, or a server that dropped, is in that state while it waits for its next retry. The framework closes a server for good withconnection.close()in three places:src/framework.ts:17636). When the server's first connect had failed, the connection there is the stubconnectWithReconnectreturned. The stub stays registered, and a laterconnectMcplServerof its id fails withMCPL server "<id>" is already registered(src/mcpl/server-registry.ts:42) untildisconnectMcplServerorrestartMcplServerdrops it. Greptile found this in its review of fix(mcpl): an MCPL server's boolean settings are true or false, and anything else is refused (#379) #451."close"fallback, after a manifest change (src/framework.ts:17800) and at the initial policy (:17918). The server answered the policy request just before, so it's down there only if it dropped in between.A runtime connect that fails partway leaves its gate standing.
connectMcplServerInternalinstalls the data-plane gate before it awaits accounting (src/framework.ts:17616), and installing it pauses every registered server's data plane (:17444). The connect completes the gate only once its policy registration has returned (:17644-17645).failMcplDataPlaneGatewhen their accounting fails:resume(),syncDiscordAwarenessMarkers(), tools-list reconciliation and reconnect reconciliation (:5137,:17175,:18498,:18553).Either way, every other server's pushes, incoming messages and inference requests are held until some other gate completes.
A connection closed for good can still let out what it held.
ready()andreadyControlPlane()don't check whether the connection is closed (src/mcpl/server-connection.ts:346,:380), and a close that fails keeps its registry entry (src/mcpl/server-registry.ts:157-160). So the next gate's completion flushes what that connection held, with its policy already reset.Changes
McplServerRegistry.closeServer(id, connection), which is no longer private and has a doc comment. It drops the entry onceclose()resolves, whatever the connection reports, if that connection is still the one registered under the id. A close that fails keeps the entry and rejects, and a close already under way is shared, as before.failMcplDataPlaneGate, then rethrows. While the host serves, that recycles the running servers, as on the four other paths: each withreconnect: truereconnects, and its reconnect's own gate holds its data until that reconnect's accounting succeeds. While the host is stopping, it releases the gate.McplServerConnection.ready()andreadyControlPlane()now do nothing once the connection is closed with reconnect off, as afterclose(), whether or not its transport close succeeded, or after a drop with reconnect off. A connection between retries keeps reconnect on and readies as before.closeFailedRuntimeConnect, which logs a close that fails instead of discarding it. Such a close keeps the entry, ascloseServerdoes for every caller, so a later disconnect, restart or shutdown can close it again.McplServerConnection.close()'s comment now says only the registry'scloseAllbounds its caller's wait.What callers will notice
connectMcplServerorrestartMcplServerfails its awareness accounting or its policy registration, connecting the same id again works, where it failed as already registered. If the close of the failed connection fails too, its entry stays and the failure is logged, so the id is free again once a disconnect, restart or shutdown has closed it.mcpl:server-errortrace for each that readsDiscord awareness accounting unhealthy: …, as the four other runtime paths already do. A running server withoutreconnect: trueis closed for good there. Onmain, those servers stayed connected with their data held. The connect or restart rejects once that recycle has closed the running servers' transports, asresume()andsyncDiscordAwarenessMarkers()return only after theirs.main, the new server stayed connected and registered, and every server's data stayed held behind that gate.McplServerConnection.ready()andreadyControlPlane()now do nothing once the connection is closed for good. What it held stays held. Before, any gate's completion passed the part of it that needs no grant to listeners, as for a connection whose close failed and so stayed registered. While the host stops, a connection closed for good, such as one being closed, no longer passes what it held to the stopping refusal, whose answers can't be sent on a closed connection. What every other connection held reaches it as before."close"fallback leaves the registry too.McplServerRegistryisn't exported from the package entry.What each part is for
closeServer, rather than havingclose()emit'close'for a connection that's down. A connection that already reported its drop would report a second close, and every'close'listener would run twice for one drop, the framework'smcpl:server-closedandmodule:removedtraces among them.closeServeralready drops the entry forremoveServerandcloseAll, and drops it only while that connection is still the registered one, which a restart could have replaced.removeServer(id)at those places. It closes whatever is registered under the id, which may no longer be this connection.failMcplDataPlaneGatewhen accounting fails, rather than completing the gate. Completing it would release the other servers' data while accounting is failing, which the catch exists to refuse, and it would make this the one runtime gate that fails open. Its cost is the one the four other paths already pay: a running server withoutreconnect: trueis closed for good.removeServerorcloseAll. One check inready()andreadyControlPlane()covers them all.connect-failed, reaches its listeners.reconnect: trueends closed, neither connected nor retrying.syncDiscordAwarenessMarkers(), and that the failed close is logged.closeServerleaves an entry that a newer connection holds under the same id, which is why the three places use it rather thanremoveServer(id)."close"fallback must leave it unregistered. The fakes'close()emits nothing, as a connection's does when it's down between retries.Tests
At
5822b438, on Linux, Node 24.22.0. The pull request's CI runs the whole suite on Node 20 and 24.shutdown-errors29 tests,mcpl-manifest-changed8,mcpl-config-diagnostics12 andmcpl-awareness-barrier34. At8628705b, whosesrc/differs from this head's only in comments, the 31 other test files whose names start withmcpl-,stop-with-orhost-quiescepass too.main'ssrc/framework.ts,src/mcpl/server-registry.tsandsrc/mcpl/server-connection.ts, withcloseServermade public so the registry test compiles, 7 tests fail: both fallback tests, the four runtime-connect tests and the stop-path test. The registry test passes there, sincemain'scloseServeralready holds its guard.5822b438: the check inready()(the failed-close test and the stop-path test); the check inreadyControlPlane()(the stop-path test); and the check's reconnect condition, without which a connection between retries would count as closed for good (mcpl-reconnect-events' test of a reconnect stub);76798228: the policy exit's gate completion, and a gate failure in its place (both policy-registration tests); and a failed close's error escapingcloseFailedRuntimeConnect(the failed-close test, whose connect then rejects with the close's error);c87c6436: the failed connect's close going straight to the connection, as onmain(the runtime-connect helper's trace check); the accounting exit's gate failure (the stub and gate tests);closeServer's guard (the registry test); the close and the gate's failure in swapped order (the helper's trace check); and the policy exit's close (the policy-registration test);afa80b53, this pull request's second commit: either"close"fallback's change (its fallback test).reconnect: true, then a runtime connect whose first connect fails and whose accounting then fails:8202015e, this pull request's first commit, the failed connect's gate is still the current one after the rejection, and the running server is still connected with its data plane paused, the same 3 s later;5822b438, that server is closed and unregistered by the time the connect rejects.Not verified
"close"fallback isn't staged. The fallback tests' fake connection, whoseclose()emits nothing, stands in for it.shutdown-errors.test.tspins the registry keeping such an entry, throughcloseAll.Out of scope
mcpl:server-closedormodule:removedtrace follows the ones its drop emitted, as whendisconnectMcplServerremoves a server that's down, onmain.main, where this catch awaited the same close.main'sStdioTransport.close()sends one SIGTERM and waits for the child's exit with no bound; feat(mcp): modern MCP (2026-07-28) servers beside MCPL, over stdio and Streamable HTTP #262's, at its headc7eda6ce, escalates to SIGKILL and bounds the wait.Related
connectMcplServerInternalat its head43c56c42, and with it the catch this change edits for a failed accounting. Its two"close"fallbacks still callconnection.close()there.connectMcplServerInternal, adding modern MCP servers. At its headc7eda6ce, the two exits this pull request changes are as onmain.Written by Effie-2756 (Claude Opus 5.5), on a design Ada-1017 (Claude Opus 5.5) approved. Naomi-2422 (Claude Opus 5.5) reviewed the change.