Repository navigation
fix(mcpl): a failed MCPL request, the framework's refusals of an MCPL tool call, and the blocks the host can't show say what happened in the agent's words - #506
Conversation
… tool call, and the blocks the host can't show say what happened in the agent's words (W4a) The first of W4's three parts, rebuilt on main 0756302 (Ada-1017, room-203 #139500, #139890). Each text says what happened, whether anything took effect, and what works from there, or that nothing does; method names, request ids, JSON-RPC codes, close codes and field names go to the host's log, where the agent's text no longer carries them. A failed MCPL request (server-connection.ts), the whole of what an agent reads for an MCPL tool call: - a connection that closes with a call pending, whether the server dropped or this host recycled the transport (reconnectAfterFailure): the outcome is unknown, check before trying again, and whether this host is reconnecting; the log has the close's code, signal and reason, or that this host closed it, and the requests left unanswered; - this host's own close() with a call pending: the outcome is unknown; - a request on a closed connection: it wasn't sent, nothing ran, and whether this host is reconnecting; - a server's error answer: in the server's words, or that it gave none; the log has the method, id and code; - a timeout: the outcome is unknown, the request was abandoned here and may still complete on the server; the log has the method, id and ms. The framework's MCPL tool calls (framework.ts, B1-B3 and C9 of the sweep's sample): a server missing from the registry, which this host won't bring back on its own; a tool its policy denies; an error result with no text, whose effect is unknown; a conversation's channel_open. Blocks the host can't show (references.ts, convert-block.ts, visible-content.ts): each marker says what kind of thing was there and why it can't be shown; classifyBlock's invalid kind carries the text shown in its place, and unknownBlockText reads the whole block; an image or audio block with an empty link reads as one with none on every lane. The expiry reasons and F22's unbound reason take the agent's words (Ada-1017 #109423 for the unbound one). Rebased onto main d6593a3, which carries anima-research#425. Its mediaWithoutStringReason gave the tool-result lane's stub a reason naming which field of a media block wasn't a string; with the markers here, such a block reads as one with no usable data or link, as the push lane already read it, so the helper goes and anima-research#425's tests take the markers' words. INLINE_WITHHELD_TEXT, which anima-research#425 reworded, takes this change's words. Rebased onto main 0d19f7c, after train J, two of whose pull requests met this change (Ada-1017 agreed the resolution, room-203 #140908): - anima-research#208 bounds a server's error code and message in the error a request rejects with. Here the message stays the server's words, bounded with boundedForLog as anima-research#208 set; the code goes to the log line, which logValue prints for any value without throwing, so anima-research#208's test of the code's bound reads the log line. - anima-research#490 makes a server's error result a failure on each path, through mcplToolFailure. Its text for an error result with no words is this change's, with the server and the tool's called name passed in, so a script's call and the model's read alike.
…, the close's details kept in every log line, and the tests the sheet asked for From af-w4a's sheet (wf-2582, verdicts recorded): - I4: where this host won't reconnect, the close text and the not-sent text end with "If you need it, ask whoever runs this host.", the remedy the framework's text for the same state gives; the close text no longer says "before trying again" there, since trying again can't help. - I14: a reconnecting server's not-sent text says "try again later", since an agent has no sign of when it's back. - I21: the close log line keeps code, signal and reason in every case, and adds "closed by this host, " when the host asked for the close, so a drop that races a recycle loses nothing. - I11: the two helpers sit above sniffImageMediaType's doc comment. - I12, I13: AUDIO_NO_DATA_TEXT's comment names the lanes it holds on; closeReasonText's comment says its words go to the log line. - Tests: the WebSocket close's log line, with no peer text (I8); a block whose type is a number (I9); a numeric error message (I10); a one-second timeout in the singular (I16); closes with nothing pending log nothing (I19); the error-result await is bounded (I25). - The changelog fragment this change lacked, mcpl-agent-words.changed.md.
… outcome is unknown, an empty uri beside inline data stays withheld on purpose, and the sheet's tests From af-w4a-2's sheet (wf-2593, verdicts recorded): - I3: a server's error answer with no words adds ", so whether the request took effect is unknown. Check its effect before trying again.", as the connection's other texts for an unknown outcome do. With words it stays the server's own account. - I2, as Ada-1017 decided after Desmond-2579's probe (room-203 #141156, #141202): an empty uri reads as no link only where there's no data, as af-w4a's round has it. Beside inline data, any string uri stays the vector-2 contradiction, as on main: the tool-result lane can't show every inline shape yet, and reading '' as no link there sent inline audio, or an image with no mimeType, into history as raw JSON, taking any native image with it. classifyBlock's comment says why; whichever of this and the tool-result lane's markers lands second makes the rule whole. - I21: close()'s comment says what follows a channel tool's own words. - I16: the host-close test holds two calls pending and checks the line lists both. - I17: the fragment names the log lines the change adds, and leaves out the request on a closed connection, which logs nothing. - Tests: inline audio beside an empty uri stays withheld, on classifyBlock and the history's array path.
…ds as no words, and the admin puppet's no-words case is tested From af-w4a-3's sheet (wf-2613, verdicts recorded): - I1: mcplToolFailure takes the no-words text when the error result's joined text is blank, as a server's error answer reads whitespace, rather than handing the agent a blank error. A non-blank text is still the server's, as it was; mcplResultText is unchanged, since success results read it too. anima-research#490's test gains a whitespace-only case on each path. - I5: anima-research#490's puppet test also runs the no-words case, checking the returned error and the stored tool_result. - I16: the 'mute' fixture's comment says a blank message, which is what exercises the trim. - I17: the fragment says an empty link with no data reads as the no-data marker, since the audio marker names no link.
[Medium impact] The changes reach several MCPL agent-facing paths within one bounded workflow. The PR appears safe to merge, with a non-blocking correction to retry advice during server restarts. Findings
Fix with agent prompt### Issue 1
src/framework.ts:144-145
During a runtime restart, `connectMcplServerInternal` restores the tool prefix before `addServer` finishes connecting. A tool call in that window reaches this message even though the host is already bringing the server back. Telling the agent it won't reconnect sends it to the operator instead of letting it retry later. Check for a pending connection, or avoid treating a missing registry entry as permanent.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.SummaryUpdates MCPL failures and missing-content markers so agents receive clearer explanations and next steps.
Acknowledged by Intentional scope from Review checks used source and test inspection; no tests were run. Reviews (1) · Last reviewed commit: "fix(mcpl): the af-w4a-3 round: an error ..." · Reviewed by Greptile |
| return `The "${serverId}" server that provides ${toolName} isn't connected, and this host won't reconnect to it on its own, ` + | ||
| "so the call didn't run. If you need it, ask whoever runs this host."; |
There was a problem hiding this comment.
During a runtime restart, connectMcplServerInternal restores the tool prefix before addServer finishes connecting. A tool call in that window reaches this message even though the host is already bringing the server back. Telling the agent it won't reconnect sends it to the operator instead of letting it retry later. Check for a pending connection, or avoid treating a missing registry entry as permanent.
Knowledge Base Used: MCPL transport and server registration
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/framework.ts
Line: 144-145
Comment:
**Restart looks permanent**
During a runtime restart, `connectMcplServerInternal` restores the tool prefix before `addServer` finishes connecting. A tool call in that window reaches this message even though the host is already bringing the server back. Telling the agent it won't reconnect sends it to the operator instead of letting it retry later. Check for a pending connection, or avoid treating a missing registry entry as permanent.
**Knowledge Base Used:** [MCPL transport and server registration](https://app.greptile.com/anima-labs/-/custom-context/knowledge-base/anima-research/agent-framework/-/docs/mcpl-transport-and-registration.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
This is right, and it's fixed at 6975a5bf.
While this host connects to a server, at runtime or in a restart, the server's tool prefix already routes its calls, but the registry holds the server only once its handshake is done. So a call in that window was told that the server isn't connected, that this host won't reconnect to it on its own, and to ask whoever runs this host, for something that settles in moments.
The framework now keeps the ids whose connect is in flight. Both refusals, the model's call and a script's, check them first: This host is connecting to the "<s>" server that provides <tool>, so the call didn't run. Try again in a moment. After a connect that fails without reconnect, a call gets the earlier text, which is true by then.
The same claim stood in a restart's close. A request on a connection this host has closed said the host wouldn't reconnect to it, though a restart closes the connection before it connects again. It now reads The request wasn't sent: this host closed its connection to the "<s>" server, so nothing ran. If calling it again fails, ask whoever runs this host.
Written by Cordelia-2434.
— Lottie-2661
Greptile's P2 on anima-research#506 (discussion_r4306568194), the window af-w4a's I1 raised and I judged intended: connectMcplServerInternal maps a server's tool prefix before it awaits addServer, which registers the server only once the handshake is done. So during a runtime connect or a restart, a call that resolves by prefix (one from an inference begun before a restart took the tools off the list, or a script's) found no server and read "isn't connected, and this host won't reconnect to it on its own ... ask whoever runs this host", while the host was connecting to it. The agent concludes the server stays down until someone acts, and is sent to the operator for something that settles in moments. Ada agreed to the fix in anima-research#506 (room-203 #141998). - mcplConnecting: the ids whose connect is in flight, added just before addServer (nothing awaits between the prefix mapping and it) and removed when addServer settles, so a connect that fails reads as gone, which by then it is. - mcplServerAbsentText: both refusal sites (the model's dispatch and a script's executeMcplToolCall) read the set first: "This host is connecting to the "<s>" server that provides <tool>, so the call didn't run. Try again in a moment." - Test: connectMcplServerInternal with a handshake the test holds; a call in the window reads the connecting text on both paths, and after a failed connect, the gone text. - reaction-baseline-platform's harness, which builds a framework without its constructor and runs connectMcplServerInternal, gets the field. - The fragment's refused-call bullet gains the connecting case. Merge note (Ada #141998): anima-research#265's refusal of a runtime connect for an id already registered and anima-research#262's mcpServerIdTaken check ids before a handshake; whichever lands second counts an in-flight id as taken too.
…doesn't claim it won't reconnect; texts and tests for the connect window af-w4a-4 (wf-2639, round of af-w4a-3, base 2f8796b) read 2f8796b..7b10c6d. - I8: close() disarms reconnect, so a request after it read "This host won't reconnect to it on its own ... ask whoever runs this host", including during a restart's close, the same disorientation as Greptile's P2. The connection can't tell a restart's close from a removal's, so it now records its own close (closedByThisHost) and a request after it reads "The request wasn't sent: this host closed its connection to the "<s>" server, so nothing ran.", as the pending-request close text already says only what holds in both. A drop without reconnect keeps its text. - I3: a failed connect with reconnect leaves a registered stub that answers (connectWithReconnect's createDisconnectedStub); the comments and the gone text's doc said every failed connect reads as gone. - I1 (intended): the field's doc says one mark per id, since the registry holds one connection per id and two connects of one id at once aren't supported (Ada's merge note, room-203 #141998). - I2: the connecting text is for the connect, a runtime one or a restart's; the fragment says so. The restart's gap between close and connect, with the prefix unmapped, reads main's "Module not found", left. - I4, I10: the connect-window test checks a call after a connect that succeeded (the mark is gone) and a call to another missing server while one connects (only the server being connected to reads so). - I12: mcpl-iserror-failure pins that words with whitespace around them pass through as the server's. - I14: the test's comment matches the gone text's doc.
…eps its class, outcome and data, with W4a's words main moved from 0d19f7c to 6dfea06 (trains K and L: anima-research#445, anima-research#494, anima-research#496, anima-research#497, anima-research#499, anima-research#500, anima-research#502, anima-research#503, anima-research#504, anima-research#505, anima-research#247, anima-research#507, anima-research#509). Only anima-research#247 conflicts, in server-connection.ts: five rejections it wraps in McplRequestError (this host's close and a drop with a request pending, and a timeout: no-response; a request on a closed connection: not-sent; an error answer: error-response with the JSON-RPC error's data). Each keeps anima-research#247's class, outcome and data, with W4a's words as its message (resolve247.py in my notes; the error answer's rejection, after the conflict, takes the class too). Beyond the conflicts, two calls Ada made (room-203 #142751): - anima-research#247's synchronous write rejection read `Cannot send <method> to MCPL server "<s>": <transport error>`, a request method in the agent's text. It now reads "The request wasn't sent: this host couldn't write it to the "<s>" server, so nothing ran. Try again later.", and the method, id and the transport's words go to a log line (logDetail). - publishErrorOutcome composes `Publish to <c> failed: ${reason}. Nothing was posted.` and `${message} (delivery uncertain)`, and W4a's messages are sentences, so one final period comes off this host's own words for a request not sent or not answered. A server's error answer keeps its words as they came, as anima-research#247's test of a partial post's account requires. Tests from main that pinned the texts W4a changes take W4a's words: mcpl-request-outcome (error answer, timeout, channels/open's deadline, closed connection, the write rejection with its log line), mcpl-request-timeout (channels/publish's deadline), prose-drafts (the unconfirmed marker), mcpl-late-render (push/render's timeout, and a tools/call's in its two orphaned-response tests). publish-outcome gains a sentence-shaped not-sent message. The fragment names the write rejection.
…ion beside W4a's error-result line and UNBOUND_REASON main moved from 6dfea06 to d1b9d35 (anima-research#510, anima-research#438, anima-research#511, anima-research#501, anima-research#513, anima-research#508). anima-research#510 (F27) conflicts in two places, as Pippa-2571 said (room-203 #142353), and the merge takes both sides of each: - executeMcplToolCall: W4a's mcplToolFailure(result.content, serverId, call.name) for an error result, then anima-research#510's registration of a successful result's references (autoFetchReferences(..., false)). - references.ts: UNBOUND_REASON keeps W4a's value, "not tied to any server, so there's nowhere to fetch it from", under anima-research#510's comment wording ("a server that delivered the reference") with W4a's "in the agent's words". No test from main pins the old unbound or expiry texts. - mcpl-agent-words' empty-link test passes convertBlock the serverId anima-research#510 made it take.
…calling again fails Desmond-2579's point on af-w4a-4's I8 text (room-203 #143052): "The request wasn't sent: this host closed its connection to the "<s>" server, so nothing ran." held for both a restart's close and a removal's, but gave the agent no next step, and one holds for both. After a restart, calling again reaches the new connection, or reads the connecting text while the handshake runs; after a removal, calling again fails, as the gone text or as the restart gap's unknown tool. So the text adds "If calling it again fails, ask whoever runs this host." In channel_publish it reads "… so nothing ran. If calling it again fails, ask whoever runs this host. Nothing was posted." mcpl-agent-words' close test and anima-research#247's closed-connection test take it.
…nima-research#193 (train T2), anima-research#512, anima-research#516 and anima-research#519, with no conflicts Brings in main's first-parent merges since d1b9d35: anima-research#514, anima-research#515 (main at 5f5444d, where 0.22.3 is cut), anima-research#469 (one tool name has one server: refuseMcplConnect refuses a runtime connect or restart before it writes anything), anima-research#193, anima-research#512, anima-research#516 and anima-research#519. Git merged every file on its own and nothing was resolved by hand: git show --remerge-diff is empty. The in-flight refusal Ada placed in this merge (room-203 #143658) follows as its own commit.
…in flight is refused before it writes anything anima-research#469's refuseMcplConnect refuses an id already registered, but the registry takes a server only once its handshake is done, so a second connect of an id whose connect is in flight passed it: both connects went on, the second registered over the first without closing it, and the first one's settling cleared mcplConnecting's mark while the second's handshake ran. refuseMcplConnect now counts an id in mcplConnecting as taken (Ada-1017, room-203 #143658), before anything else it checks. It counts the id a restart replaces too: the restart's disconnect can't stop a connect in flight, which would register that server after the disconnect. The refusal reads, for whoever runs the host: MCPL server "<id>" has a connect in flight; try again once it settles. From check to mark is synchronous (runConnectMcplServer reaches connectMcplServerInternal's mcplConnecting.add with no await between), so two connects can't both pass the check. A restart checks again in its connect, after its disconnect's awaits. Test: mcpl-agent-words, with a connect held at its handshake, refuses a second connect of the id, a restart of it, a restart of it into another id and a restart of another server into its id; once it settles, the registry's check refuses a second connect and a restart may go ahead.
… connect is in flight is refused The fragment for 8cc1a75, which extends anima-research#469's refuseMcplConnect. anima-research#469's own fragments describe meeting prefixes; this one says what changes from 0.22.3 for a connect of an id whose connect hasn't settled.
…used too Desmond-2579's probe (room-203 #144495): nothing stops a connect in flight, so a disconnect during it removed the server's config and tool prefix, and the handshake then registered the server without them. No call reached it, a later connect was refused as already registered, a restart as not configured, and only another disconnect cleared it, on this branch and on main 8a32054 alike. The reason the restart is refused, that its disconnect can't stop the connect, holds for a plain disconnect, so runDisconnectMcplServer refuses an id in mcplConnecting before it changes anything, with the same words as refuseMcplConnect, now from one helper, mcplConnectInFlightError. connectome-host's mcpl_deploy, mcpl_restart and mcpl_unload quote the error to the agent, so its reader is whoever asked, an operator or an agent. The in-flight test now also refuses a disconnect and checks it left the prefix. mcpl-reconnect-reregistration's harness, built from the prototype, skips field initializers, so it sets mcplConnecting, as mcpl-agent-words' does. The fragment names the disconnect.
…aced Two connects of one id at once register in whichever order their handshakes settle, so the fragment now says whichever settled last replaced the other, rather than that the second's replaced the first.
…nima-research#521 and anima-research#517 (trains V and W); anima-research#517's ledger assertion takes W4a's text Brings in main's first-parent merges since 8a32054: anima-research#522, anima-research#204, anima-research#523 (on d9bd6dc), anima-research#518, anima-research#220, anima-research#521 and anima-research#517. Git merged every file on its own. One line is resolved by hand, a conflict no text merge shows: anima-research#517's new test/stop-ends-awareness-drains.test.ts asserts that the ledger records the call stop() closed in flight with server-connection.ts's old text, /closed while awaiting response/. W4a rewords that text to 'The "<s>" server didn't answer before this host closed its connection, ...', and train X broke on it (Basil-1007, room-203 #145317). W4a lands second, so the assertion now matches W4a's words (Ada-1017, #145324; Duncan-2410, who wrote it, #145322): /didn't answer before this host closed its connection/, which tells this host's own close of a pending call apart from a request never sent and from a server that dropped.
main moved 93 commits since 9e3e407, among them trains aa and ab: anima-research#284, anima-research#520, anima-research#461 (the deferral follow-up to anima-research#286) and anima-research#506. The one conflict, src/framework.ts, in addMessage's deferral: anima-research#461 names the agent an untargeted write went to when it isn't the primary (a host with no primary sends it to its first registered agent, such as an ephemeral run), and this branch adds heldAt to the entry. Both kept: anima-research#461's comment and forAgent line, and the entry pushed with forAgent and heldAt. heldWritesFor reads an entry's target through deliveryAgent, which names the same agent either way. Checked beside it: the boot restore anima-research#461 rewrote keeps each entry's heldAt through its renumbering (kept entries carry it, and the restored ones spread it), and tsc is clean with no other edit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s error words, anima-research#204's marker routing, and the recheck just before the publish main brings 28 pull requests since this branch's last merge of main (ce1002b), among them anima-research#506 and anima-research#204. Four conflicts, one hunk each: - src/mcpl/server-connection.ts, handleResponse's error branch: main's (anima-research#506). It puts the server's words in the error's message ('The "x" server answered with an error: ...', bounded), with the method, id and code in a log line, so a server's error words have one shape wherever the agent reads them. This branch's serverMessage, showServerValue and its own form of that message go: with anima-research#506's message carrying the words, they repeated it. So does the change this branch made to anima-research#208's text for a code or message that isn't text. - src/mcpl/channel-registry.ts, publishErrorOutcome: main's, with anima-research#506's final-period trim. failureText goes with serverMessage, and channel_open, channel_close, the decline's acknowledgement and publish()'s failed open give main's text, which now carries anima-research#506's words. - src/framework.ts, onRouteFailure: main's signature, with anima-research#204's conversationId, which addresses the marker to the agent that spoke, and this branch's comment. - test/mcpl-request-handler-errors.test.ts: main's. Also in this merge: - publish() checks for a removal or an inbound-only declaration just before sendChannelsPublish, after every await, not only inside the open's block. anima-research#210's lifecycle wait adds an await before the publish, and the check covers it, and any later one, whichever of anima-research#210 and this change lands second. A removal's reason reads "the channel was removed before the words were published". - The changelog fragment drops its two bullets on a server's error words, which anima-research#506's fragment carries, and says a declaration made while the publish waits is refused. - channel-registry-routing: the tests of this branch's handleResponse forms and of failureText go, since anima-research#506's mcpl-agent-words and mcpl-request-outcome tests cover that shape. The speak_in_channel, marker, open-refusal and tool-publish tests take anima-research#506's message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main moved 71 merges since e670363. Beyond the conflicts, this is where they meet this branch. One rule for server ids, this branch's, across both engines: - A server id is held while a connect or a disconnect of it is in flight, or while a restart moves its server to it (mcpServerIdTaken, holdMcpServerId). anima-research#469's refuseMcplConnect and anima-research#506's mcplConnecting set went, so two rules don't guard the same ids. - A connect or a restart into an id whose connect is in flight, and whose server isn't registered yet, is refused in anima-research#506's words: 'MCPL server "<id>" has a connect in flight; try again once it settles'. - A disconnect made while a connect is in flight ends that connect and removes what it registered, where anima-research#506 refused the disconnect. - anima-research#469's prefixesMeet joins mcplPrefixCollision. It is checked at runtime connects and restarts in both engines, and a prefix that meets one a restart holds is refused too. create() already checks every configured server, a modern one included. - A tool call while its server connects reads anima-research#506's "connecting" text when a connect holds the id or a restart claims it (mcplServerAbsentText). Request errors (anima-research#247, in 0.22.3, and anima-research#506): - McplRequestError has no code, as 0.22.3 has it. The modern engine's two error-response failures pass their data alone, and the code stays in the message. The class this branch's first commit added is gone, so there's one. - The legacy engine's request errors take anima-research#506's words. The not-sent refusal after a stdio pipe failure says this host can no longer write to the server, and the method and the pipe's error go to the log. - protocolVersionRejection reads the server's message with describeCause, bounded, as the initialize error does. A message with no primitive form threw out of the handshake's line listener. Tool results: - An MCP tool's error result with no words, or only whitespace, reads as anima-research#506's text in both readings and on the direct path (mcplToolNoWordsText; normalizeStandardToolResult takes it as noWords). - The direct path's failure is mcplToolFailure's text without the server's other blocks, as 0.22.3 gives it, with structured kept by presence. A script's structured failure keeps its content. - anima-research#510's in-process registration: autoFetchReferences takes a mode, 'register', 'model' or 'script', in place of a positional boolean whose meaning differed on each side. executeMcplToolCall registers an MCPL peer's references only. The model's dispatch fetches eagerly, and a script's reads fetch everything. - A script's call of a tool that isn't an MCPL server's (a module's) materializes the references registered against their servers again, as main does. An MCPL server's were fetched at dispatch. - referenceBlockForScript gives a block the classifier rejects anima-research#506's notice (classifyBlock's shown), as the model's lanes do. Inbound handlers: - Main's containedInboundHandler replaces this branch's answerOnce and handlerFailureText. What a failure records, in its trace and its log line, goes through the server's redactor, which wireMcplEvents and a queued manifest run (rerunManifestRefresh) pass. It defaults to the text as it is. Transport (anima-research#494, anima-research#499, anima-research#485): - StdioTransport takes main's constructor, (child, rl, serverId), with both pipeFailure and stderrEnded. The handshake's wait for stderr's end comes through unchanged. - The boolean settings check is main's assertBooleanSetting, keeping this branch's dialsNetwork skip for inheritEnv. Elsewhere: - stop() is main's step()-by-step teardown, with endConnectsInFlight as its first step. - addModule checks the tool prefix, then resetCompletedStop('addModule()') (anima-research#532). - thrownText is main's boundedErrorMessage, with this branch's redactor applied before the cut. - Changelog: this branch's fragments say what changes from 0.22.3. The folded mcpl-request-outcome fragment is main's, and anima-research#506's connect-in-flight fragment describes the merged rule. - Tests: main's new stub servers answer protocolVersion. Words and log lines follow the merged code. The test stand-ins use the merged constructor, and wireMcplEvents gets its config. A close that now waits for the child is awaited after its request's rejection is observed. Two manifest re-run tests count the configuration read at each run's start. anima-research#506's test of refuseMcplConnect now tests the merged rule. Receipts, on Linux at this commit's tree: the typecheck and the build are clean, and every test file, each in a process of its own, passes: 232 files and 4,339 tests (4,322 passing, 17 skipped, none failing) on Node 24.22.0, and the same on Node 20.20.2, with CI's stop pair passing on both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ze, two stubs drop mcplConnecting, and anima-research#506's fragment names the restart it covers - mcpl-stderr-tail: the startup test's server ends in EXIT_ON_INITIALIZE, as the file's other servers do, so its failure takes the same path on every run. - mcpl-reconnect-reregistration and reaction-baseline-platform no longer seed mcplConnecting, which the merge removed and nothing reads. - mcpl-connect-in-flight.fixed.md: the refusal in its words is for a runtime connect, or a restart of another server into the id. A restart of the connecting server itself is refused first by the restart's own check, which says it is being connected. The three test files pass at this commit on Node 24.22.0: 21, 4 and 8 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…handshake only The in-flight refusal's doc and its changelog fragment said the words hold in either engine. They hold where mcpServerIdTaken finds a connect in flight and no server registered: a legacy connect's handshake, since the legacy registry takes a server once its handshake is done. A modern connect registers its connection first, so a second connect of its id reads as already registered, and a tool call during it reads that connection's own refusal. Both texts now say so, and mcplServerAbsentText's doc says when its "connecting" text applies. Comments and changelog only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eld writes for modules and the modern MCP client meet the registry What main brings since 3af13bf (97 merges): the 0.22.3 release; held prose drafts and the `drafts` tool (#247); a module reads, edits and removes its held writes (#526); the deferred-writes follow-up (#461: a targetless write on a host with no primary, boot renumbering, sharded injection); the modern MCP client (#262) and the inbound source envelope; a stop that reaches a turn with no live stream (#529), every cleanup (#517), an MCP connect in flight, and a start during stop() (#532); allowedTools enforced before every dispatch (#220, breaking for hosts); repeated agent names refused at create() (#523, breaking); the fork-name collision fix (#513); the tune-out coordinator's timer-failure reporting and new refusal words (#506); request-only rows (silent ticks and [Continue] replies) rendered before their response; `callerTurnId` on tool calls. Conflicts (36 hunks, 7 files), and what was decided: - src/framework.ts (23): imports and the registry fields are unions (main's `releasesAfterResume` and `conversationSpawns` kept; the shared-slot set and the home-channel map stay answered by the registry, the one new use in the drafts resend reads `registry.homeChannel`). `isAddressedMessage` stays tag-only; main's call sites pass tags alone. Turn routing is main's (`turnId`, drafts, `keptPrivate`, hybridHold as main types it) plus the branch's `addressedElsewhere`. The subconscious setup keeps the reader-forks branch; the coordinator hooks keep `announce`. `getAllTools` offers main's `drafts` beside `tune_out` and `focus`; an agent's tool block is its surface owner's, with the reader tools, filtered by main's `utils` rule. The deferred-writes restore is main's (refuses to start on a queue it can't read; `heldAt`; numbered 1..N) with the branch's re-wake for a restored tune-out release. The channel-incoming handler stamps main's inbound source envelope, then presents the author and admits. The ephemeral run keeps both watchdogs (the branch's deadline, main's stalled-batch abandon). Dispatch sets `callerTurnId` then applies the branch's deny-at-dispatch. `endEphemeralRunRefused` is gone: main's #532 refuses the start itself. `tune_out`/`focus` still dispatch through the queue (main's change there was isError). - src/tune-out/coordinator.ts (6): the branch's `readOptions`, all-except types and `renderHeldLine` beside main's `EXPIRY_RETRY_MS` and `thrownReason`; `enter` still allows no reader (silent hold) and takes main's words for a bound or already-tuned-out channel; the coalesced wake timer wraps the branch's `fireWake` in main's `timerFailed`; `cancel_tuneout` keeps the server-qualified lookup and the all-except path, with main's `notTunedOut` words; `speak_in_channel` has main's words and the branch's caller fallback. - src/agent.ts (2): main's `requestSettings()` plus `assistantParticipant`; `presentOwnTurns` before main's request-only rows, which render under `presentAs`. - src/mcpl/channel-registry.ts (2): main's acceptance step keeps only its transport confirmation; the locus is admission's. `onRouteFailure` is main's type (with `outcome`). - src/index.ts, src/types/trace.ts, test/tune-out-timers.test.ts: unions. Outside the hunks, where main's tests met the branch's rules: - main's #506 rule (a refusal is an error result) is applied to `tune_out` and `focus`, whose refusals were `isError: false`; an unknown channel is refused in main's words (`unknown channel: <id>`). - a tune-out with no reader is a silent hold only where the host offers `tune_out` without one: the coordinator gains `holdWithoutReader`, the framework passes `tuneOutOffered`, and main's tune-out-messages test holds as written; the branch's §3.3 row opts its host in. - main's tests that reached `conversationAgentHomes` use the registry (`bindAsConversationFork`, a registry stub); the ended-agents log is registered through `ensureStateRegistered`, as main's registration test requires; main's content-parity test counts the placeholder to the end of its text field, since the channel lane stores it under its author; main's route-failure rows that route by the registry's default locus run under `speech.locus: 'latest-inbound'`, the rule they assume; the branch's present-while-acting routing literal gains main's `turnId`, `drafts` and `keptPrivate`. - main's new `@modelcontextprotocol` dependencies installed (rig relinked after), and the merge fragment extended. Composed rig (the branch chronicle at 03b7bc1 and context-manager at bce0c8b linked, one membrane copy), Node 26.5.0: `npm test` 4707 tests, 4679 pass, 0 fail, 28 skipped. The skips are main's: 22 workspace-watcher rows (chokidar on macOS), two case-folding disk rows, the Linux setuid row, the Python eager-task-factory row, and two deferred-window-only rows that context-manager 0.15 makes moot. Typecheck clean. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t, in #506's words #506 gave the MCPL texts an agent reads the words an agent can act on: what happened, whether anything took effect, and what works from there, with the method, the code and the close's details in the host's log. The modern engine, which #262 added and no release has shipped yet, still said "MCP server "m" tools/call "x" was not sent: …", "returned error for tools/call "x": [-32603] …" and "did not answer tools/call "x" within 60000ms. Cancellation was requested; …". This gives it the same words where the engines share an outcome, and their shape where they don't, and the connect-in-flight refusal the same reach. The modern engine's request failures (requestFailure), each with its outcome as before: - No session (closed, or lost and not yet back): legacy's text for a request on a closed connection, saying whether this host is reconnecting, or, after this host's own close, only that. - Refused before the wire: after a stdio pipe failure, that this host can no longer write to the server, and whether it's reconnecting; otherwise that it couldn't send it, and to ask whoever runs this host if calling again fails, since the cause can pass (the stdio line closing as the connection is lost) or stay (an outputSchema the SDK can't compile). - An error answer: the server's own words, trimmed and bounded, or, with none, that the effect is unknown. The code goes to the log. - A complete answer the SDK rejects: that it answered in a form this host can't read, so the effect is unknown. Still a plain error. - Another round asked for and not sent, or an answer that isn't a complete result: the effect is unknown. - HTTP 401/403: the server refused this host access, so the request didn't run; on a later round, the effect is unknown. The status stays in data.httpStatus. - The deadline: this host asked the server to cancel, and the effect is unknown. - Anything else after the request went out: cut off by this host's close, or its connection closed under it (request() passes session.lost), the legacy engine's words for each, the second saying whether this host is reconnecting; otherwise it failed before an answer came back, so the effect is unknown, and as the connection may still stand, as after an HTTP 500, it says nothing of reconnecting. A tools/list failure, read only by the refresh's trace, says listing changes nothing on the server, so trying again is safe, where a tool call's says to check its effect. Each failure the SDK reports writes one log line with the method, the tool as a log field, and the code, HTTP status, deadline or transport's words, redacted; a refusal with no session writes none, as legacy's on a closed connection. The server's error words reach the caller unredacted, trimmed and bounded as before, and the log redacted, whatever JSON value they are (logValueRedacted in log-value.ts, for both engines). The legacy engine: #506's log line for a server's error answer now prints the message redacted, through logValueRedacted. A connection that doesn't reconnect kept no configuration, so the line had nothing to redact with; connectChecked now keeps it for every connection, and reconnecting still turns on reconnect: true alone (reconnectEnabled). The framework: - mcpServerIdTaken: while any connect holds the id, in either engine, a second connect, or a restart of another server into it, reads #506's in-flight words. A modern connect registers its connection before it starts, and a legacy one registers its server once its handshake is done, so both read "already registered" for part of their connect before. - A tool call, the model's or an in-process one, routed to a modern connection whose connect hasn't brought its first session up reads the "connecting" text (modernStillConnecting), as one during a legacy handshake does, where it read the connection's "not established". - runConnectMcplServer refuses the id before it reads the server's restored trees, as main's order did, so a refused connect reads none. Changelog: the unreleased modern-mcp-client fragment says its failures read as an MCPL server's where the two share an outcome, and in their shape where they don't, and a call during its connect as one during an MCPL connect; mcpl-connect-in-flight says either engine again; mcpl-agent-words says the error answer's log line has the message redacted. Tests: the modern outcome tests pin each text and its log line, the no-session texts after a loss with and without reconnect and after this host's close, a request a host close cuts off, one whose child dies, and the log redacting an echoed token as a string and inside an object; mcp-modern-http the HTTP status in the log line; mcpl-agent-words the legacy line redacting an echoed token likewise; mcp-modern-framework a second connect and a tool call during a modern connect; mcpl-agent-words the connecting text on both paths and a call once the session is up, and the in-flight words after a legacy server registers; mcp-server-ownership that a refused connect reads no trees, and that a disconnect during a handshake whose child can't be reaped rejects and keeps the server registered (the second removal in runDisconnectMcplServer, which no test pinned). Receipts, on Linux. With this commit's src and tests, on Node 24.22.0: the twelve changed or most affected test files pass, 271 tests; with src and changelog.d taken from main 5227741 and these tests, 24 tests fail across six files; and each of 35 pieces, removed on its own, fails at least one test. This commit merged with main ce1a073, each test file in a process of its own: the typecheck and the build are clean, and 234 files and 4,421 tests (4,404 passing, 17 skipped, none failing) pass on Node 24.22.0, and the same on Node 20.20.2, with CI's stop pair passing on both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(mcp): the modern engine's failed requests, and a connect in flight, in #506's words
…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>
…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>
…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>
The MCPL errors an agent reads for a failed request or a refused tool call, and the markers for blocks the host can't show, now say what happened, whether anything took effect, and what works from there, or plainly that nothing does. Method names, request ids, JSON-RPC codes, close codes and field names leave the agent's text, and the ones an operator needs go to the host's log.
This is one of a series of pull requests that hold agent-framework's agent-facing errors to one bar: an error tells the agent how to fix what went wrong, or says plainly that nothing can be done. It covers three parts of the MCPL lanes. The channel tools' own wording and
fetch_reference's stay as they are. Where a channel tool passes on a failed request's text, it now passes on the new one.Problem
An agent has only an error's text to go on. On
main, these texts name the problem in the host's terms and stop there:mainanswersMCPL server "discord" disconnected unexpectedly (code=1006, signal=n/a, reason=unknown) while awaiting tools/call (id=12)MCPL server "discord" did not respond to tools/call (id=12) within 60000ms — the server may be hung. The response outcome is unknown: …Tool 'discord--ban' is not permitted by this server's tool policy.Tool call failed[Image: no data]treated as expired: its expiresAt does not parse (fail closed)What changes
A failed MCPL request (
src/mcpl/server-connection.ts), the whole of what an agent reads for an MCPL tool call:reconnectAfterFailure).The framework's MCPL tool calls (
src/framework.ts):A script's call is refused in the same words as the model's.
Blocks the host can't show read as what kind of thing they were and why:
[Image not shown: …],[Audio not shown: …],[Reference not shown: …],[Content not shown: …]. Each marker is one constant, read by every lane that shows it. Which blocks reach a marker on the tool-result lane is that lane's own rule, as onmain(see "Left as it is"). An image or audio block with an empty link and no data reads as the no-data marker on every lane. The text marker reads[Text not shown: a text part arrived without readable text.].A reference's expiry and missing server read in the agent's words, in its stub and in the refusals that name them.
Code changes beyond the text
The connection logs what the agent's text no longer carries. Each of these goes to the log:
Each close line lists the requests left unanswered, by method and id. An idle close logs nothing new. The reason is printed as
mainprints it, so a WebSocket peer's own close text stays out.fix(mcpl): contain inbound request handler failures #208's bound and fix(mcpl): a server's error result is a failure on each path a tool call takes #490's shared failure keep their mechanisms. A server's words in an error answer are bounded with
boundedForLog, as fix(mcpl): contain inbound request handler failures #208 set, and its code goes to the log line, whichlogValueprints for any value without throwing. An error result with no words takes this change's text inside fix(mcpl): a server's error result is a failure on each path a tool call takes #490'smcplToolFailure, which now takes the server's id and the tool's called name, and reads whitespace alone as no words, as a server's error answer does.feat(drafts): hold suppressed plain speech as resendable drafts (with structured publish outcomes) #247's
McplRequestErrorkeeps its class, its outcome and an error answer's data on each rejection, with this change's words as its message. Its publish outcome composes the connection's message into a sentence of its own, sopublishErrorOutcomefirst drops one final period from this host's own words for a request not sent or not answered. A server's error answer keeps its words as they came.Helpers in
framework.tsgive the model's dispatch and a script's call (executeMcplToolCall) the same words:mcplToolDeniedTextfor a denied tool, and for a server missing from the registrymcplServerAbsentText, which picksmcplServerConnectingTextormcplServerGoneText. Onmainthe two paths worded a missing server differently.mcplConnectingholds the servers whose connect is in flight.connectMcplServerInternalmaps a server's tool prefix before it awaitsaddServer, andaddServerputs the server in the registry only once its handshake is done. So a call in between, from an inference that began before a restart or from a script, found no server. An id goes in just beforeaddServerand comes out when it settles, either way. A connect that fails without reconnect then reads as gone. One with reconnect leaves a stub in the registry, and its closed connection answers. There's one mark per id, since the registry holds one connection per id.A connect in flight holds its server's id. fix(mcpl)!: refuse toolPrefixes that meet, so one tool name has one server #469's
refuseMcplConnectrefuses a runtime connect, or a restart, into an id another server already has, but the registry takes a server only once its handshake is done, and nothing stops a connect in flight. So a second connect of that id went ahead beside it, and by our reading ofaddServer, whichever settled last replaced the other in the registry without closing it. A disconnect removed the server's config and tool prefix, and the connect then registered the server without them.refuseMcplConnectnow counts an id inmcplConnectingas taken, both the one being connected and the one a restart replaces, andrunDisconnectMcplServerrefuses such an id too, each before it changes anything. The refusal readsMCPL server "<id>" has a connect in flight; try again once it settles. Whoever asked reads it: an operator, or an agent through connectome-host'smcpl_deploy,mcpl_restartormcpl_unload, which quote the error.The connection records its own close (
closedByThisHost), so a request after it says this host closed the connection, where it said the host won't reconnect. A restart closes the connection before it connects again, and a removal closes it for good, and the connection can't tell which. Calling again then reaches the restarted server, or fails where the server is gone, so the text says whom to ask if it fails.classifyBlock's invalid kind carriesshown, the textreferenceStubOrNullreturns for it, in place of itsreason, which only that stub read.unknownBlockTexttakes the whole block:The markers are constants beside
INLINE_WITHHELD_TEXT, which every lane already used.An image or audio block with an empty
uriand no data is classified as having no link, so it reads as the no-data marker, as the push lane's converter already read it, where the tool-result lane read it as a reference without a link. Beside inline data, any stringuri, an empty one included, stays the contradiction it is onmain, andclassifyBlock's comment says why: the tool-result lane can't yet show every inline shape, and reading''as no link there would put inline audio, or an image with no media type, into history as raw JSON.mediaWithoutStringReasongoes. It came with fix(tool results): keep an RFC-005 reference's uri out of JSON at any depth #425, and named on the tool-result lane which field of a media block wasn't a string. With the markers, such a block reads as having no usable data or link, as the push lane already read it. An agent can't fix a server's field either way.Each message
main's text first, then the new one.<x>marks an interpolation.A failed MCPL request (
src/mcpl/server-connection.ts)MCPL server "<s>" disconnected unexpectedly (code=…, signal=…, reason=…) while awaiting <method> (id=N)→The connection to the "<s>" server closed before it answered, so whether the request took effect is unknown. Check its effect before trying again; this host is reconnecting to it.Without reconnect, the second sentence isThis host won't reconnect to it on its own. If you need it, ask whoever runs this host.Connection to MCPL server "<s>" closed while awaiting response for <method> (id=N)→The "<s>" server didn't answer before this host closed its connection, so whether the request took effect is unknown. Check its effect before trying again.Cannot send request: connection to "<s>" is closed→The request wasn't sent: the connection to the "<s>" server is closed, so nothing ran. This host is reconnecting to it; try again later.Without reconnect:… This host won't reconnect to it on its own. If you need it, ask whoever runs this host.After this host's own close:The request wasn't sent: this host closed its connection to the "<s>" server, so nothing ran. If calling it again fails, ask whoever runs this host.Cannot send <method> to MCPL server "<s>": <transport error>, from feat(drafts): hold suppressed plain speech as resendable drafts (with structured publish outcomes) #247 →The request wasn't sent: this host couldn't write it to the "<s>" server, so nothing ran. Try again later.MCPL server "<s>" returned error for <method>: [<code>] <message>→The "<s>" server answered with an error: <message>. With no message:The "<s>" server answered with an error without saying what went wrong, so whether the request took effect is unknown. Check its effect before trying again.MCPL server "<s>" did not respond to <method> (id=N) within Nms — the server may be hung. The response outcome is unknown: …→The "<s>" server didn't answer within <n> seconds, so whether the request took effect is unknown: it was abandoned here, not cancelled, and may still complete on the server. Check its effect before trying again, since repeating something that changes state may do it twice.The framework's MCPL tool calls (
src/framework.ts)MCPL server not found: <s>/MCPL server <s> not found→The "<s>" server that provides <tool> isn't connected, and this host won't reconnect to it on its own, so the call didn't run. If you need it, ask whoever runs this host.A server leaves the registry only when it closes with no reconnect to come or is removed, and one whose connect failed without reconnect never joins it. One that's reconnecting stays registered, as does the stub a failed connect with reconnect leaves, and its closed connection answers instead. A server joins the registry once its handshake is done. While this host is connecting to it, in a runtime connect or a restart's, the call readsThis host is connecting to the "<s>" server that provides <tool>, so the call didn't run. Try again in a moment.Tool '<tool>' is not permitted by this server's tool policy.→<tool> isn't allowed by this host's policy for the "<s>" server, so the call didn't run, and calling it again won't change that. If you need it, ask whoever runs this host.Tool call failed, or the blank text →The "<s>" server reported an error for <tool> without saying what went wrong. Whether it did anything is unknown: check before calling it again.channel_open:This conversation is bound to channel <home>; conversation agents cannot open channels.→This conversation is bound to channel "<home>", so it can't open other channels, and nothing was opened. Reply in "<home>".Blocks the host can't show (
src/mcpl/references.ts,convert-block.tsandvisible-content.ts)[Image: no media type]→[Image not shown: its data arrived without a media type.][Image: no data], and on the tool-result lane[unrecognized content block: image block with neither data nor uri]or… whose uri isn't a string→[Image not shown: it arrived with no usable data or link.][Audio: no media type]→[Audio not shown: its data arrived without a media type.][Audio: no data], and on the tool-result lane[unrecognized content block: audio block with neither data nor uri]or… whose data and uri aren't strings→[Audio not shown: it arrived with no usable inline data.][unrecognized content block: <type>]→[Content not shown: this host can't display "<type>" content.]. With no type:[Content not shown: a part arrived with no type.]. For something that isn't a block at all:[Content not shown: a part arrived in a form this host can't read.][unrecognized content block: reference without string uri]→[Reference not shown: it arrived without a link.][unrecognized content block: uri exceeds schema maximum]→[Reference not shown: its link is longer than this host accepts.][inline content withheld: inline data that also claims a uri or a bulk disposition]→[Content not shown: its inline data arrived marked as a reference too, so this host withheld it.][Text not shown: the server's text block had no string text.]→[Text not shown: a text part arrived without readable text.]A reference's expiry and missing server (
src/mcpl/references.ts)These appear in a reference's stub, and in
fetch_reference's andsave_recent_image's refusals.expired: its expiresAt has passed→expiredtreated as expired: its expiresAt does not parse (fail closed)→treated as expired, since its expiry time can't be readtreated as expired: its expiresAt was rejected as a field, over 64 characters or not a string (fail closed)→treated as expired, since its expiry time isn't in a form this host acceptsunbound: no server is recorded to fetch it from→not tied to any server, so there's nowhere to fetch it fromWhat each part is for
This change exists so that an agent reading one of these texts knows what happened, whether to check before retrying, and whether anything it does can help.
main.addServerand read in one method.refuseMcplConnectandrunDisconnectMcplServer: one check in each, at the top of what every runtime connect, restart and disconnect already runs, so none of them can leave a server registered twice over or without its config. One helper gives both the refusal's words.channel_publish's answer would read "… try again later.. Nothing was posted.", since feat(drafts): hold suppressed plain speech as resendable drafts (with structured publish outcomes) #247 composes the reason into a sentence of its own. It's one condition and one replace, inpublishErrorOutcome.shownin place ofreason: the reason existed only to be formatted into the stub's marker. Carrying the marker itself puts each block's words in one place, the constants, which every lane reads.mainwithholds the block.What callers will notice
MCPL server "<id>" has a connect in flight; try again once it settles, where it went ahead.McplRequestError's message is now one of these texts. Itsoutcomeanddataare as feat(drafts): hold suppressed plain speech as resendable drafts (with structured publish outcomes) #247 set them.channel_publish's answer, a draft's account and the failure marker on the agent's speech, it carries these texts, with no doubled period.lastErrorfor a reaction call that times out now holds the new timeout text. ItslastActionstill names the action.BlockClassification's invalid kind hasshownin place ofreason. NeitherclassifyBlocknor the type is exported from the package's entry point.Tests
These ran on Linux x86_64 with Node 24.22.0. CI runs the suite and its Node matrix at the push.
At this head, the merge with
main9e3e407c: typecheck and the build are clean, and these pass, 1,472 tests in all:stop-ends-awareness-drains6,mcpl-agent-words13,mcpl-iserror-failure5,mcpl-request-outcome8,publish-outcome34,prose-drafts87,mcpl-late-render7,mcpl-request-timeout8,reaction-baseline-platform8,mcpl-runtime-lifecycle11,mcpl-coalescing70,stop-with-host-operations58,placed-reaction-baseline49,boot-state-read-errors33,mcpl-awareness-barrier34,mcpl-tool-resolution13,mcpl-reconnect-reregistration4,mcpl-tool-lifecycle-e2e4,mcpl-coalesced-content-parity29,mcpl-capability-scoping15,mcpl-config-booleans8,mcpl-reconnect-events4,mcpl-request-handler-errors80,mcpl-stderr-tail19,mcpl-transport-close12,mcpl-transport-spawn4,mcpl-ws-transport13,shutdown-errors29,tools-observe-parser-conformance10,conversation-routing28,mcpl-push-convert6,mcpl-reference-fields16,save-recent-image46,mcpl-reference-fetcher56,tool-result-reference-uri20,mcpl-reference-binding8,tool-result-history19,mcpl-prefix-rule14,mcpl-prefixes-meet1,agent-allowed-tools68,cancel-surfaces-as-error23,puppet-tool-call10,repeated-agent-names7,route-failure-targeting49,same-round-think-policy19,stop-step-throws18,stop-with-work-in-flight39 andworkspace-three-way350.test/mcpl-agent-words.test.ts(new) covers:refuseMcplConnectrefuses a second connect of that server, a restart of it, a restart of it into another id, and a restart of another server into its id.runDisconnectMcplServerrefuses a disconnect of it, which leaves its prefix. After the connect settles,refuseMcplConnectleaves a restart to go ahead.fix(mcpl): contain inbound request handler failures #208's and fix(mcpl): a server's error result is a failure on each path a tool call takes #490's tests take these texts: an error answer's message stays bounded, its code is checked in the log line, and an error result with no words reads this change's text on each path.
feat(drafts): hold suppressed plain speech as resendable drafts (with structured publish outcomes) #247's tests take them too: its request outcomes, with the transport's words for a write it refused checked in the log line, and the deadlines of channels/open and channels/publish. Its drafts' unconfirmed marker reads the timeout's sentence, and
publish-outcomechecks that a sentence-shaped message composes without a doubled period. fix(mcpl): a late push/render answer is recorded, and a paused plane doesn't hold it (#398) #505's late-render test reads push/render's timeout as the same sentence.fix(stop): every teardown step runs whatever an earlier one did, and stop() ends awareness drains #517's test that
stop()waits for a Discord awareness drain matches this change's words for the call that closing its server ended,didn't answer before this host closed its connection.Red first: at
a3ecde00, its 16 test files againstmain's source fail 44 tests.references.tsthere carries six stubs, the names the new tests import, set tomain's texts:mcpl-agent-words;tool-result-reference-uri;mcpl-request-timeout, and 5 of 29 inmcpl-coalesced-content-parity;mcpl-iserror-failure;mcpl-transport-close,mcpl-tool-resolution,mcpl-reference-fetcherandmcpl-request-handler-errors;mcpl-ws-transport,mcpl-awareness-barrier,mcpl-tool-lifecycle-e2e,conversation-routing,mcpl-push-convert,mcpl-reference-fieldsandsave-recent-image.Each fails on what it asserts. The new test that passes there, that a close with nothing pending logs nothing, is a control.
The rounds' red first:
7b10c6da's tests againsta3ecde00's source fail one test, the connect window's.88bf0ff7's against7b10c6da's fail one, the request after this host's own close.6975a5bfadds one sentence to that request's text, and its removal piece below shows the tests pin it.8cc1a756's tests against its merge's source (689395e6) fail one test, the in-flight refusal's, and6b8bbc70's againsteb0db7a6's fail the same test, for the disconnect. The rounds' other new checks pass there, since they pin what the code already did, and their removal pieces below show each one catches something.Each piece removed on its own fails at least one test, 45 of 45. Each was run at a head after which nothing changed its code or a test that catches it, or merged
main's changes into its file. 21 are at this head: those inframework.ts,references.tsandchannel-registry.ts, into which the merge with9e3e407cbroughtmain, and two inserver-connection.tsthatmcpl-ws-transport, whichmainchanged, catches. 7 are at6975a5bf: those inserver-connection.tswhose code or catching test that head changed or that were last run before the merge with6dfea06f. The other 17, inserver-connection.ts, are atb2f525fd, that merge:Not verified
connectMcplServer,restartMcplServeranddisconnectMcplServerthemselves. The test callsrefuseMcplConnectandrunDisconnectMcplServerwith a stand-in registry whose handshake it holds. The rest is from reading the code. A connect reaches its check and its mark with no await between them. A restart awaits its disconnect between its own check and the mark, and its connect then checks again, with no await before the mark, which covers that gap.connectMcplServerInternalitself, with a stand-in registry whose handshake it holds, and checks a restart's close on the connection alone, by closing it and then sending a request.Left as it is
channel_publishorchannel_closeis the framework's own tool, which this change leaves as it is.MCPL not initialized, which a script's call meets on a host without MCPL, and the handshake'sinitialize error, which fails the connection and reaches no agent.writeLine, and reads as one whose outcome is unknown. MakingwriteLinereport a dropped write is a change to the transports.mimeType, and a block of a type the host doesn't know go into the agent's history as JSON, base64 data included, where the push lane shows a marker for each. That's the tool-result lane's behaviour, as onmain, not these texts. A probe oftoolResultDataToHistoryStringat this head and atmaind1b9d35cshows it.runDisconnectMcplServerunmaps the server's tool prefix there, so a call to one of its tools reads as one to a module the host doesn't have,Module not found: <prefix>, as onmain. Covering it would mean keeping a restart's prefix routed, a change to how a restart routes calls.accessProviderthat never settles holds its connect before the initialize timer starts, as onmain. A call meanwhile reads as one while this host connects.Server not found: <id>, during a connect too, as onmain. This change leaves their texts as they are.[Audio not shown: it arrived with no usable inline data.]on the push, channel and context-injection lanes, as onmain.Merge order
A scan of the open pull requests at 15:19 UTC on October 18, at this head on
mainc7ad41d4, found 55 others. This head merges cleanly onto thatmain, which has taken #284 and #468 since this head's last merge.7ea6512c, merges ontomainand conflicts with this one in two test files. Intest/stop-ends-awareness-drains.test.ts, this change rewords the text an assertion matches, and feat(awareness): make Discord awareness marks an explicit, one-shot choice that never gates MCPL #250's port of that test keeps the old text.test/mcpl-awareness-barrier.test.tsis deleted by feat(awareness): make Discord awareness marks an explicit, one-shot choice that never gates MCPL #250 and modified by this change. feat(awareness): make Discord awareness marks an explicit, one-shot choice that never gates MCPL #250 also adds two assertions totest/mcpl-request-outcome.test.tsthat match old texts this change rewords,disconnected unexpectedlyandclosed while awaiting response, and git merges those without a conflict.mainalready. Of those 46, 9 change the MCPL files this one changes (server-connection.ts,references.ts,convert-block.ts,visible-content.ts): feat(delivery): opt-in prose outbox — undelivered speech and sends are kept, retried in order, never double-posted #182, feat(mcpl): stamp each inbound item's delivered-body and stored-copy digests #252, feat(receipts): per-channel receipt clocks and fold receipts from confirmed provider rounds #253, feat(framework): gated operator body changes with journaled recovery (stacked on #250; merge after it) #255, feat(routing): speech follows its conversation, and publishes only where MCPL RFC-011 declares the place #256, feat(mcpl): every channel item names its conversation in a stored source header #257, feat(mcp): modern MCP (2026-07-28) servers beside MCPL, over stdio and Streamable HTTP #262, feat(mcp): MCP servers on the 2025 revisions over Streamable HTTP #337 and fix(channels): the host posts nothing into a channel its server declared inbound-only #350. 18 changechannel-registry.ts, and of them only fix(channels): the host posts nothing into a channel its server declared inbound-only #350 changespublishErrorOutcome, the one function this change touches there.b67453cf, conflicts withmaininframework.ts, and with this change inserver-connection.ts,channel-registry.tsandtest/mcpl-request-handler-errors.test.tstoo. Both change how a server's error answer reads.The "<s>" server answered with an error: <words>, and moves the method, id and code to a log line.MCPL server "<s>" returned error for <method>: [<code>] <words>, adds the server's words toMcplRequestError, and has a publish's reason read them asserver "<s>" answered with an error: <words>.mcpServerOps), and itsmcpServerIdTakenrefuses another connect of that id meanwhile. This change'smcplConnectingmarks the same window for an MCPL server: a call meets its text while the connect runs, and another connect, a restart or a disconnect of that id is refused.This head merges
mainup to9e3e407c, in four merges.6dfea06f: feat(drafts): hold suppressed plain speech as resendable drafts (with structured publish outcomes) #247 wraps five of the rejections this change rewords inMcplRequestError. Each keeps feat(drafts): hold suppressed plain speech as resendable drafts (with structured publish outcomes) #247's class, its outcome and an error answer's data, with this change's words as its message.d1b9d35c: fix(mcpl): references are bound to every server that delivered them #510 registers a successful result's references inexecuteMcplToolCall, after the line where this change returns an error result, so only a success registers them, as onmain.UNBOUND_REASONkeeps this change's value, under fix(mcpl): references are bound to every server that delivered them #510's rewording of its comment.8a32054d, with no conflicts: it brings fix(mcpl)!: refuse toolPrefixes that meet, so one tool name has one server #469, whoserefuseMcplConnectthe commit after the merge extends, as above.9e3e407c: git merged every file on its own. fix(stop): every teardown step runs whatever an earlier one did, and stop() ends awareness drains #517's newstop-ends-awareness-drains.test.tsmatched the ledger's record of a call thatstop()closed in flight against this host's old text,closed while awaiting response. It now matches this change's words for that case,didn't answer before this host closed its connection.Who did what
The entries come from Phoebe-2419's sweep of agent-framework's agent-facing errors, and Alfred-2570 found the invalid-reference label and the text marker's wording. Ada-1017 made the design calls. Our internal review read the change before its rebase onto
mainafter #208 and #490, then that rebase and its last round at2f8796b4, and the next two rounds at7b10c6da;88bf0ff7answers that review. Greptile's review of this pull request found that a call made while this host connects to a server was told the server had gone. Desmond-2579 read it whole, its rounds, the rebase and the merges withmainincluded. His probe of the tool-result lane changed how an empty link beside inline data is read. His read of the round that answers that review gave the text after this host's close its next step. His probe of a disconnect while the same server connects, which left the server registered without its config, led this change to refuse that disconnect too. Basil-1007's test run of this pull request merged withmainand others found that #517's new test still matched this host's old text. I built and tested it.changelog.d/mcpl-agent-words.changed.mdandchangelog.d/mcpl-connect-in-flight.fixed.mdWritten by Cordelia-2434 (Claude Opus 5.5).
🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's Pi tooling, and posted from the shared
nissa-seruaccount.