Repository navigation
fix(mcpl): manifestChanged without params, WebSocket close reasons in errors, and per-pause drop counts - #450
Merged
nissa-seru merged 3 commits intoOct 17, 2026
Conversation
…ike any other handleManifestChanged read announce.domains for its receipt only after it had applied the fetched manifest: the grant, the feature sets and the policy request. A notification may carry no params, and the wiring passes them through as they arrive, so an announcement without params reached the handler as undefined and threw a TypeError there. The receipt was never emitted and handleToolsListChanged never ran, though the grant had changed; the listener's catch logged "manifest refresh failed". The handler now reads the announcement's domains first, through announcedDomainsOf: the strings of its params' `domains` array, or none when the params are missing or malformed. The receipt's announcedDomains is string[] in its trace type, so a `domains` that isn't an array of strings no longer reaches it as whatever the server sent. The listener and the handler take the params as unknown, which is what they are. Tests: an announcement without params gets one receipt naming no domains, with the fetched manifest applied and the tools list refreshed; params that are null or a string, or domains that aren't an array, name none, and an array holding non-strings reports only its strings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a close makes A WebSocket transport marks a close reason the peer sent as the server's own text (reasonFromPeer). mcpl:server-closed leaves that text out and keeps to the transport's words, but two errors still put it in raw: the connect error when the transport closes during the handshake, and the error every pending request is rejected with when the connection drops. The connect error is the error a connect-failed or reconnect-failed event carries, which the host traces as mcpl:server-connect-failed and, from a reconnect's fifth failed attempt, posts in the mcpl-down alert; a request's error reaches its caller, and an MCPL tool call's catch puts it in the agent's tool result. A reason the server writes with a line break prints a second line that reads as the host's own. Both now take the reason through closeReasonText: the transport's words, and for a peer's reason the words the transport uses for a close without one, "websocket closed". A WebSocket close's code, a number, says what the server meant (4001: a refused credential), with or without a reason, so each error keeps it: a request's error has code= already, and the handshake's adds it for any WebSocket close, "(websocket closed, code 4001)", since a close before the handshake completes reaches no 'close' event. On main the handshake's error gave no code, with or without a reason. The transport's kind tells a WebSocket close from a stdio child's exit, whose error reads as before. Tests: a server that closes with code 4001 during the handshake fails the connect with "(websocket closed, code 4001)", both with a two-line reason and with none, and one that closes with that reason while a tools/call waits rejects it with "reason=websocket closed"; neither error holds any of the server's text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While a plane is paused, a busy server's responder-less events are buffered up to MAX_BUFFERED_EVENTS, and past it the oldest is dropped (issue anima-research#122). droppedBufferedEvents counted those drops for the connection's whole life and was never reset. So the flush line that ends a later pause reported every earlier pause's drops as its own ("225 dropped at cap" for a pause that dropped 75), and the drop log, which speaks at the first drop and at every hundredth after it, counted across pauses too. ready(), which ends a pause by flushing its buffer, now reads the count for its line and resets it, so each pause reports and logs its own drops. A control-plane-only flush leaves the data plane paused, and its count running. Test: two pauses past the cap. The first drops 150 and its flush line says 150; the second drops 50, runs through a control-only flush and a second pauseDataPlane(), drops 25 more, and its flush line says 75. The drop log speaks at the first pause's 1st and 101st drop and at the second pause's 1st. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Required label not found on this PR. |
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 17, 2026
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 17, 2026
…#286's drained writes, anima-research#450's announcement checks and anima-research#451's config checks Three conflicts in src/framework.ts, each resolved to keep both sides: - The imports: this branch's errors import beside anima-research#451's assertServerConfigBooleans. - After bySeq: this branch's InboundResponder beside anima-research#286's DrainedWrite and asCompiled. - The manifest-changed listener: this branch's contained form, taking its params as unknown, as anima-research#450 made them. A refresh that rejects now emits mcpl:request-handler-error and a stderr line through the containment, as every inbound handler's failure does, rather than anima-research#450's console line. Outside the conflicts, rerunManifestRefresh takes the announcement as unknown: anima-research#450's handleManifestChanged reads it through announcedDomainsOf, and its two schedulers now pass it on as unknown. 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 17, 2026
…'s default branch names, anima-research#426's failed-create release, anima-research#450's close reasons and anima-research#451's config checks Conflicts, and what each keeps: - src/framework.ts imports: this branch's McplRequestError beside anima-research#451's assertServerConfigBooleans. - initializeMcpl's connect catch: main's rethrow of a DiscordAwarenessAccountingError (anima-research#426), so an accounting failure during a startup connect ends startup, and create()'s release closes the servers. - initializeMcpl after its connects, and a runtime connect's tail: this branch's, with no data-plane gate. main changed only comments there, and anima-research#426 dropped the startup gate's closeAll in favour of create()'s release. - runRestartMcplServer: anima-research#451's config check before the disconnect, then this branch's journal read, for a server that derives the protective default only. - src/mcpl/server-connection.ts, a lost connection's rejection: this branch's McplRequestError ('no-response') with anima-research#450's closeReasonText. - test/fixtures/awareness-barrier-mcpl-server.mjs stays deleted, with the barrier it served. Carried from main: - anima-research#405's default branch names now pass over a name a suppression whose removals aren't recorded as finished claims, so a default name is never refused for a claim. While the journal can't be read, a claim counts for nothing in the choice, and checkDiscordAwarenessBranchName refuses the name with the journal's message. The helper names rollbacks, suppressions, turn undos and a host command's message undos. The offline recovery's default name follows the same rule once its journal is open; a dry run doesn't open the journal, so its default name passes over held branches only. - anima-research#426's test that a create() failing after its MCPL server connected closes the server: this branch's startup reconciles awareness before any MCPL server connects, so the test injects its failure into the tool discovery that follows the connects, and reads the handshake from the reregistering fixture's events. - anima-research#405's test that an explicit name stays the caller's: this branch refuses a known collision before anything is prepared, so the refusal it expects is "Rollback branch chosen already exists" (invalid), not the store's. New tests in operator-branch-names: a default rollback name a pending suppression claims takes the next free name; while the journal can't be read, a default name is refused with the journal's own message; an offline recovery's default name a pending suppression claims takes the next free name, while a dry run's passes over held branches only; and while the journal can't be read, an offline recovery's default name is refused with the journal's own message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 17, 2026
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 17, 2026
main had taken 123 commits since 2482625, among them skip_reply's refused wake (anima-research#198), the deferral rules (anima-research#286) and the close reason a pending request's rejection names (anima-research#450). Conflicts, both sides kept: - prose-segments: anima-research#198's withoutRoundsProse beside anima-research#247's splitProseRuns. The framework imports both, and XML mode's turn-end runs are anima-research#247's runs of the turn's content without the refused rounds' prose, as main's segments are. - XML mode's turn-end silencing scan: anima-research#247's cause (silenceCauseOf) over the calls main's scan counts, a refused skip_reply left out. - driveStream's finally: anima-research#247's drafts settlement, then main's fork disposal (anima-research#286), both after main's turn-end flush. A closing fork's closure turn lands its notices in the fork's store, records its drafts as noticed, and then the fork goes. - channel-registry: anima-research#247's acceptInbound beside anima-research#198's channelTools. - server-connection: a disconnect rejects a pending request with anima-research#247's McplRequestError ('no-response'), its text naming anima-research#450's closeReasonText. - prose-segments tests: anima-research#247's trimmed-runs helper and tests beside anima-research#198's withoutRoundsProse tests. Beyond the conflicts: anima-research#198 saves the silence before a refused wake's round and restores it at the next round, as a boolean. anima-research#247 keeps why a turn is silenced beside it (an explicit send holds the prose as drafts; skip_reply keeps it private), so restoring the boolean alone left the refused round's 'private' standing, and a send after a refused wake had its prose kept private instead of held. The saved silence now carries its cause, and the next round restores both. A test in prose-drafts pins it in native and in XML tool mode. 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 91e70ed, among it: the check of a server's boolean settings (anima-research#451), the release of a failed create() (anima-research#426), a broken stdin or stdout ending a stdio child's connection (anima-research#384), the stderr tail a failed handshake keeps (anima-research#399), a WebSocket close's reason (anima-research#450) and unbound references (anima-research#404). Conflicts, resolved by hand: - src/mcpl/server-registry.ts: this branch's structure (adding, register, holdsUnreapedChild) with anima-research#451's two reads, `config.reconnect === true` at the connect and `!connection.willReconnect` at the close. - src/framework.ts: - The startup catch and the startup barrier take main's form: an accounting failure or a failed barrier ends startup, and create()'s release closes every server through stop(), both engines' connections among them. This branch's closeMcpConnectionsAfterFailedStartup goes, with its test: stop() closes both engines' connections, each whatever the other's close did, and what it couldn't close is in create()'s rejection, where the old cleanup logged it. - The startup catch keeps this branch's redaction and willRetry, and takes main's stderr tail, redacted in its log line and its trace. - restartMcplServer checks the boolean settings beside this branch's checks, before the running server is torn down. - The legacy connect and reconnect failures' traces carry main's stderrTail, redacted, and the alert still leaves it out. - stop() resets main's stopFinishedTeardown, then ends the connects in flight. - handleManifestChanged reads its configuration and the announced domains before anything else. - src/mcpl/server-connection.ts: the handshake keeps this branch's protocolVersion and owner's close, and takes main's stderr tail on its transport failures. connectWithReconnect keeps this branch's throws, with `config.reconnect !== true`. The initial connect's and a reconnect attempt's log lines and events carry the stderr tail. A pending request's error is this branch's McplRequestError with main's closeReasonText. withStderr takes the server's redactor, so no log line prints the tail unredacted. - src/mcpl/types.ts: requestTimeoutMs's doc keeps both sides. - The tests: the reference fields' imports, and transport-close's `within` with main's bound. - changelog.d/stop-waits-for-operator-operations.fixed.md was released in 0.22.2. This branch's change to it, stop() ending a connect in flight, moves to a fragment of its own. Beyond the conflicts, so that the two sides agree: - anima-research#451's check skips inheritEnv for every network server, an http(s) one included, since only the stdio spawn reads it. - A script's view of an MCPL tool result registers its references bound to their server, as anima-research#404's lanes register the model's, so its expired reason is the stub's. The dispatch's fetch pass had bound each reference a script reads already. - anima-research#384's handling of a broken stdin or stdout (the child ended, its close naming the error, no 'error' event) replaces this branch's, so two of its tests now expect the close's reason. - Main's tests that wire a legacy connection pass the configuration this branch's wiring takes, and build a stand-in StdioTransport with its server id. The stderr tail's tests check its redaction in the log lines and the trace, and a new one checks it at startup. - The redaction changelog fragment names the stderr tail. Checks, on Linux: - Each of 8 pieces the merge adds, removed on its own, fails a test on Node 24.22.0: the stderr tail's redaction in the initial connect's, a reconnect's and the startup's log lines, in the startup trace, and in the legacy connect and reconnect traces; a script's reference binding; and the inheritEnv skip for an http(s) server. - The typecheck and the build pass, and every test file passes, each in a process of its own and unforced: 182 files and 2,940 tests on Node 24.22.0, and the same on Node 20.20.2. 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.
Three small fixes to the MCPL connection, one commit each with its tests and changelog fragment, so each can be taken or left on its own. #449 describes the defects, with the runs on
mainthat show them. The new tests fail onmainat3adbdb8and pass at this head. !Fixes #449.Changes
mcpl/manifestChangedwithout params (18ff80e).handleManifestChangedread the announcement'sdomainsfor its receipt only after it had applied the fetched manifest. A notification sent without params reaches it asundefined, so it threw once the grant had changed, and emitted nomcpl:manifest-change-receiptand refreshed no tools list. It now reads the announcement first, throughannouncedDomainsOf, which gives the strings of its params'domainsarray, or none when the params are missing or malformed. The receipt'sannouncedDomainsisstring[]in its trace type, and now always is one.8605165). Two errors put a WebSocket peer's close reason into their text as the server wrote it: the connect error for a transport that closes during the handshake, and the error each pending request is rejected with when the connection drops.mcpl:server-closedalready leaves that text out (reasonFromPeer). Both errors now give the transport's own words for such a close,websocket closed. They keep the close code, which says what the server meant whether or not it sent a reason (4001 marks a refused credential). A request's error already hadcode=. The handshake's error now ends(websocket closed, code 4001)for any WebSocket close, since, by our reading, a close before the handshake completes reaches no'close'event, and onmainit gave no code. The connect error reaches the host's log, themcpl:server-connect-failedtrace and themcpl-downalert, and, by our reading, a request's error reaches an MCPL tool call's result. So a reason with a line break could pass for a host log line, or mislead the model. A stdio child's exit reads as before.c15fd9d). While a plane is paused, a server's responder-less events are buffered up toMAX_BUFFERED_EVENTS, and past it the oldest is dropped.droppedBufferedEventscounted those drops over the connection's whole life. So a later pause's flush line reported earlier pauses' drops as its own, and the drop log, which speaks at the first drop and at every hundredth after it, counted across pauses too.ready(), which ends a pause, now reads the count for its line and resets it.What each part is for
Each fix was weighed against a simpler or a wider one.
announcedDomainsOf. Reading the announcement before anything is applied ends the throw after the grant has changed, and taking only the strings of an array keeps the receipt'sannouncedDomainsthestring[]its type says. Refusing an announcement without params instead would leave a server's real manifest change unapplied for want of a hint the handler doesn't need, since the handler reads the change itself from the fetched manifest.closeReasonText. One helper serves both errors, so the rulemcpl:server-closedalready follows, that a peer's close reason stays out, holds wherever a close reason becomes an error. The close code stays in each, since it's a number the server can't use to write a line, and it tells a refused credential from another close. The handshake tells a WebSocket close from a stdio exit by the transport's existingkind, so the close info needs no new field. Escaping the peer's text instead, as host log lines do withlogDetail, would keep server-written text in errors that reach the model and the alert.ready().ready()ends a pause and reads its count for the flush line, so the read and the reset are one step. Resetting inpauseDataPlane()instead would zero a pause's count whenever the data plane is paused again before it opens, and four call sites pause it: two reconnect paths (reconnectAfterFailure,reconnectOnce), a quiesce (runQuiesce) and the data-plane gate (installMcplDataPlaneGate).Overlaps
#208 rewrites the
manifest-changedlistener asonRequest('manifest-changed', ...), so the two conflict once, at that listener insrc/framework.ts. Merged together, the listener keeps #208'sonRequestform withparams: unknown.src/mcpl/server-connection.tsmerges without a conflict.Tests
mcpl-manifest-changed. An announcement without params gets one receipt naming no domains, with the fetched manifest applied and the tools list refreshed. Params that arenullor a string, ordomainsthat isn't an array, name none, and an array reports only its strings.mcpl-ws-transport. A server that closes with code 4001 during the handshake fails the connect with(websocket closed, code 4001), both with a two-line reason and with none. One that closes with that reason while atools/callwaits rejects it withreason=websocket closed. Neither error holds any of the server's text. Two pauses past the cap report their own drops, 150 and 75, the second through a control-only flush and a secondpauseDataPlane(), and the drop log speaks at each pause's first drop.mainat3adbdb8with these tests added, the six new tests fail on Linux with Node 20.20.2 and 24.22.0, each on its defect. At this head typecheck and build are clean, and our runner runs the suite eight files at a time with--test-force-exit. On Node 24.22.0 it reports 2433 of 2434 tests passing in 152 files. The one failure istool-result-guard's "fix(guard): a withheld tool result says where its original is (#277) #281: batches behind one stalled write cost one deadline: no write starts until it settles", in a file this PR doesn't change, andhistory-module-overviewstopped early at 22 of 28. On Node 20.20.2 it reports 2373 of 2373 in the 151 files that finished, andmcpl-coalescingdidn't finish within 1200 seconds. Run one at a time without that flag,history-module-overviewpasses 28 of 28 andmcpl-coalescing67 of 67 on both runtimes, andtool-result-guardpasses 124 of 124 on Node 24 and 122 of 124 on Node 20, where two of its fix(guard): a withheld tool result says where its original is (#277) #281 tests fail. CI's unforced step passes 105 of 105 on both.Implemented by Kestrel-2409 and independently reviewed by Esther-2584, both using Claude Opus 5.5. Ada-1017 filed the three defects from an internal review of #208 and made the calls on their scope.