Repository navigation
fix(framework): refuse start() and addModule() while a stop() is still running - #532
Merged
nissa-seru merged 1 commit intoOct 18, 2026
Conversation
…l running stop() claims its attempt and sets running to false before its first await, and resetCompletedStop() cleared a stop only once it had completed. So a start() made while the stop ran passed start()'s running check, reopened provider admission and started a second event loop under the stop. On main at c0155d3, with a store the framework owns: - a stop() with no budget waited on the new loop and was still pending 3 s later; with stop(1000) it rejected after 1,007 ms, "pending: event loop", and running stayed true; - a start() made while a module's stop() ran, past the stop's wait on its loop: the stop resolved and closed the store, the new loop ran on over it, every later stop() returned the same settled promise, and a further start() changed nothing, so nothing could end that loop; - an addModule() made then, after stopAll() had taken its list: the module was started, never stopped, and stayed registered. resetCompletedStop(), which start() and addModule() both pass before any work, now refuses while a stop is in flight (stopPromise set, the stop not completed), after its owned-store check: "start() refused: the framework is stopping", and the same for addModule(), which rejects. start() passes it before its running check, since stop() runs steps that can emit traces before it clears running: a start() made there, from a trace listener, is refused too. emitTrace catches a listener's throw, so that can't fail the stop. On a running framework with no stop, start() still does nothing. start() and addModule() after a settled stop() behave as before, which is anima-research#315's open question. Text that described a start() made during a stop now says it's refused: comments in stopOwned(), on guardSpillsClosedFor and in src/gate/event-gate.ts, and docs/tool-result-guard.md. stop()'s doc said a start() could be made "meanwhile" beside a loop a timed-out stop() left in a call; it means once that stop() has failed, and now says so. This is the in-flight half of anima-research#315, as proposed in a comment there. 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 18, 2026
main gained anima-research#526 (modules read, edit and remove the writes the framework holds for them), anima-research#531 (fetch_reference's failures say what happened and what calling again can do, and the refs save hook logs why a payload wasn't stored) and anima-research#532 (start() and addModule() are refused while a stop() is still running). Where they meet the include: - reference-fetcher.ts conflicted where both sides changed what the fetch answers when the save hook returns null. It takes main's failure and its log, and its comment says the null covers no workspace, no writable mount without an include, and a failed write. - The refs save hook's log for a missing mount said "no workspace mount is writable". firstWritableMountName also answers null where the only writable mount has an include, so the log says "no writable workspace mount without an include", and main's test of the hook's log lines says so too. - The rest merged on its own: docs/tool-result-guard.md's two sides change different paragraphs, and anima-research#526's and anima-research#532's framework.ts changes touch no mount or workspace path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 task done
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 18, 2026
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>
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 18, 2026
main at 5227741 brings anima-research#526, anima-research#531, anima-research#532, anima-research#535, anima-research#533 and anima-research#262. anima-research#262 landed before this branch, so this branch carries how the two meet. The one conflicting file is src/framework.ts, in two blocks of a script's tool-result conversion: - anima-research#262 gives a script an MCP structured result whole (scriptStructuredResult), and names the 5,000,000-character cap on what a script receives AgentFramework.SCRIPT_RESULT_MAX_CHARS. Both blocks take anima-research#262's side. - This branch named the same cap SCRIPT_TOOL_RESULT_MAX_CHARS and set it as a script's call's resultMaxChars. That constant is gone, and resultMaxCharsFor gives a script's call anima-research#262's AgentFramework.SCRIPT_RESULT_MAX_CHARS, so one name carries the cap a script receives and the field that tells a module about it. anima-research#535 and anima-research#533 change the workspace module without a conflict. Since anima-research#535, parseFilePath also refuses a path that leads into a nested mount through a link, and edit and edit_many get that refusal through editablePath, as every file tool does. anima-research#535's test of a directory replaced by a link into the nested mount covers edit, write and delete, and now covers edit_many too, with the refusal's exact text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 task done
tel-0s
added a commit
that referenced
this pull request
Oct 18, 2026
…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>
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 19, 2026
…anima-research#506's words and F240 main moved 331 commits on from 9e3e407, among them anima-research#262 (the modern MCP client and the legacy engine's protocol boundary), anima-research#506 (the agent's words for failed requests), anima-research#532, anima-research#537, anima-research#210 and anima-research#542 (F240: a redaction that can't read its configuration withholds the text). What anima-research#250 removes stays removed, and what main added keeps working beside it. Conflicts: - src/framework.ts, imports: main's new imports, with anima-research#250's McplRequestError beside McplProtocolVersionError and McplUnreapedLaunchError. - src/framework.ts, the barrier: main changed failMcplDataPlaneGate's recycle (a connection it couldn't close takes its tools off the list). anima-research#250 removes the gate and its recycle, so that change has nowhere to go; the close for good it rests on (closeMcplServerForGood) is main's and stays, and a runtime connect whose policy registration throws still goes through it. - src/framework.ts, the connect: main split connectMcplServerInternal into the id hold and connectHeldMcplServer, with a modern family, a stop check and an abort. That structure is main's. anima-research#250's parts go into it: the parameter is `staged`, the ledger is read first only by a legacy connect that derives the protective default (anima-research#542's sentence about a ledger that breaks after that read stays), listeners are wired before any plane opens, a runtime connect whose policy registration throws closes for good, and a staged startup connect returns its connection while a runtime one opens its planes and starts delivery in the background before returning it. - src/framework.ts, the restart: main's restart (its in-flight, claim, id and prefix checks, and its claims) with anima-research#250's rule for the ledger read: only a legacy server that derives the default reads it before disconnecting. - src/mcpl/server-connection.ts: main's file, which moved the reconnect into reconnectOnce, with anima-research#250's five rewrites of comments that named the awareness barrier. - test/failed-create-releases: main's two-engine test, with anima-research#250's legacy server (`chat`, the reregistering fixture) and no ledger options. - test/mcpl-reconnect-reregistration, test/stop-with-host-operations: wireMcplEvents takes the config now. - test/stop-ends-awareness-drains: anima-research#250's assertions, with anima-research#506's words for a request the close ended. - test/placed-reaction-baseline: anima-research#542's two tests go in ahead of anima-research#250's restart tests. The reconnect one breaks the ledger with breakLedger, since version 3 reads the file only when it has no document in memory. - test/mcpl-awareness-barrier.test.ts and its fixture stay deleted. Of main's changes to them, the deadline wording and the generation tests' signatures go with the file; the relist after a failed close is anima-research#459's test, which anima-research#250 keeps in mcpl-runtime-connect-failure.test.ts, so that assertion moves there. Outside the conflicts: - test/fixtures/reregistering-mcpl-server.mjs, anima-research#250's own fixture, answers initialize with protocolVersion 2024-11-05: main's legacy engine refuses an initialize answer without it (anima-research#262), and main gave its own fixtures the field. - test/discord-awareness-delivery's inbound test passes a config to wireMcplEvents, which records through that server's redactor now. - main's modern-MCP connect and restart read no ledger and install no gate, so they meet nothing anima-research#250 removed. Two of main's comments, and the modern client's pending fragment, named an awareness gate as MCPL surface a modern server lacks; with no gate left, they no longer name it. - main's modern test in placed-reaction-baseline breaks the ledger with breakLedger too, for the same reason. - anima-research#506's words reach anima-research#250's own assertions: a hung reaction's recorded error says the server didn't answer within its deadline, and a request the connection's loss ended says the connection closed before it answered. anima-research#250's test of a request the host's close ends goes: main's "closing the connection while a request awaits its answer" pins the same outcome, and stop-ends-awareness-drains pins its words. - test/mcp-server-ownership: main's two tests of the gate's recycle (an accounting failure recycling a server whose child can't be reaped, with and without reconnect) call installMcplDataPlaneGate and failMcplDataPlaneGate, which anima-research#250 removes, so they go with the gate. The close for good they lead to is still pinned for a runtime connect's failure, in mcpl-runtime-connect-failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 19, 2026
…arch#506's words and anima-research#542 main moved 331 commits on from 9e3e407, among them anima-research#262 (the modern MCP client and the legacy engine's protocol boundary), anima-research#506 (the agent's words for failed requests), anima-research#532, anima-research#537, anima-research#210 and anima-research#542 (a redaction that can't read its configuration withholds the text). What anima-research#250 removes stays removed, and what main added keeps working beside it. Conflicts: - src/framework.ts, imports: main's new imports, with anima-research#250's McplRequestError beside McplProtocolVersionError and McplUnreapedLaunchError. - src/framework.ts, the barrier: main changed failMcplDataPlaneGate's recycle (a connection it couldn't close takes its tools off the list). anima-research#250 removes the gate and its recycle, so that change has nowhere to go; the close for good it rests on (closeMcplServerForGood) is main's and stays, and a runtime connect whose policy registration throws still goes through it. - src/framework.ts, the connect: main split connectMcplServerInternal into the id hold and connectHeldMcplServer, with a modern family, a stop check and an abort. That structure is main's. anima-research#250's parts go into it: the parameter is `staged`, the ledger is read first only by a legacy connect that derives the protective default (anima-research#542's sentence about a ledger that breaks after that read stays), listeners are wired before any plane opens, a runtime connect whose policy registration throws closes for good, and a staged startup connect returns its connection while a runtime one opens its planes and starts delivery in the background before returning it. - src/framework.ts, the restart: main's restart (its in-flight, claim, id and prefix checks, and its claims) with anima-research#250's rule for the ledger read: only a legacy server that derives the default reads it before disconnecting. - src/mcpl/server-connection.ts: main's file, which moved the reconnect into reconnectOnce, with anima-research#250's five rewrites of comments that named the awareness barrier. - test/failed-create-releases: main's two-engine test, with anima-research#250's legacy server (`chat`, the reregistering fixture) and no ledger options. - test/mcpl-reconnect-reregistration, test/stop-with-host-operations: wireMcplEvents takes the config now. - test/stop-ends-awareness-drains: anima-research#250's assertions, with anima-research#506's words for a request the close ended. - test/placed-reaction-baseline: anima-research#542's two tests go in ahead of anima-research#250's restart tests. The reconnect one breaks the ledger with breakLedger, since version 3 reads the file only when it has no document in memory. - test/mcpl-awareness-barrier.test.ts and its fixture stay deleted. Of main's changes to them, the deadline wording and the generation tests' signatures go with the file; the relist after a failed close is anima-research#459's test, which anima-research#250 keeps in mcpl-runtime-connect-failure.test.ts, so that assertion moves there. Outside the conflicts: - test/fixtures/reregistering-mcpl-server.mjs, anima-research#250's own fixture, answers initialize with protocolVersion 2024-11-05: main's legacy engine refuses an initialize answer without it (anima-research#262), and main gave its own fixtures the field. - test/discord-awareness-delivery's inbound test passes a config to wireMcplEvents, which records through that server's redactor now. - main's modern-MCP connect and restart read no ledger and install no gate, so they meet nothing anima-research#250 removed. Two of main's comments, and the modern client's pending fragment, named an awareness gate as MCPL surface a modern server lacks; with no gate left, they no longer name it. - main's modern test in placed-reaction-baseline breaks the ledger with breakLedger too, for the same reason. - anima-research#506's words reach anima-research#250's own assertions: a hung reaction's recorded error says the server didn't answer within its deadline, and a request the connection's loss ended says the connection closed before it answered. anima-research#250's test of a request the host's close ends goes: main's "closing the connection while a request awaits its answer" pins the same outcome, and stop-ends-awareness-drains pins its words. - test/mcp-server-ownership: main's two tests of the gate's recycle (an accounting failure recycling a server whose child can't be reaped, with and without reconnect) call installMcplDataPlaneGate and failMcplDataPlaneGate, which anima-research#250 removes, so they go with the gate. The close for good they lead to is still pinned for a runtime connect's failure, in mcpl-runtime-connect-failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 19, 2026
…arch#506's words and anima-research#542 main moved 331 commits on from 9e3e407, among them anima-research#262 (the modern MCP client and the legacy engine's protocol boundary), anima-research#506 (the agent's words for failed requests), anima-research#532, anima-research#537, anima-research#210 and anima-research#542 (a redaction that can't read its configuration withholds the text). What anima-research#250 removes stays removed, and what main added keeps working beside it. Conflicts: - src/framework.ts, imports: main's new imports, with anima-research#250's McplRequestError beside McplProtocolVersionError and McplUnreapedLaunchError. - src/framework.ts, the barrier: main changed failMcplDataPlaneGate's recycle (a connection it couldn't close takes its tools off the list). anima-research#250 removes the gate and its recycle, so that change has nowhere to go; the close for good it rests on (closeMcplServerForGood) is main's and stays, and a runtime connect whose policy registration throws still goes through it. - src/framework.ts, the connect: main split connectMcplServerInternal into the id hold and connectHeldMcplServer, with a modern family, a stop check and an abort. That structure is main's. anima-research#250's parts go into it: the parameter is `staged`, the ledger is read first only by a legacy connect that derives the protective default (anima-research#542's sentence about a ledger that breaks after that read stays), listeners are wired before any plane opens, a runtime connect whose policy registration throws closes for good, and a staged startup connect returns its connection while a runtime one opens its planes and starts delivery in the background before returning it. - src/framework.ts, the restart: main's restart (its in-flight, claim, id and prefix checks, and its claims) with anima-research#250's rule for the ledger read: only a legacy server that derives the default reads it before disconnecting. - src/mcpl/server-connection.ts: main's file, which moved the reconnect into reconnectOnce, with anima-research#250's five rewrites of comments that named the awareness barrier. - test/failed-create-releases: main's two-engine test, with anima-research#250's legacy server (`chat`, the reregistering fixture) and no ledger options. - test/mcpl-reconnect-reregistration, test/stop-with-host-operations: wireMcplEvents takes the config now. - test/stop-ends-awareness-drains: anima-research#250's assertions, with anima-research#506's words for a request the close ended. - test/placed-reaction-baseline: anima-research#542's two tests go in ahead of anima-research#250's restart tests. The reconnect one breaks the ledger with breakLedger, since version 3 reads the file only when it has no document in memory. - test/mcpl-awareness-barrier.test.ts and its fixture stay deleted. Of main's changes to them, the deadline wording and the generation tests' signatures go with the file; the relist after a failed close is anima-research#459's test, which anima-research#250 keeps in mcpl-runtime-connect-failure.test.ts, so that assertion moves there. Outside the conflicts: - test/fixtures/reregistering-mcpl-server.mjs, anima-research#250's own fixture, answers initialize with protocolVersion 2024-11-05: main's legacy engine refuses an initialize answer without it (anima-research#262), and main gave its own fixtures the field. - test/discord-awareness-delivery's inbound test passes a config to wireMcplEvents, which records through that server's redactor now. - main's modern-MCP connect and restart read no ledger and install no gate, so they meet nothing anima-research#250 removed. Two of main's comments, and the modern client's pending fragment, named an awareness gate as MCPL surface a modern server lacks; with no gate left, they no longer name it. - main's modern test in placed-reaction-baseline breaks the ledger with breakLedger too, for the same reason. - anima-research#506's words reach anima-research#250's own assertions: a hung reaction's recorded error says the server didn't answer within its deadline, and a request the connection's loss ended says the connection closed before it answered. anima-research#250's test of a request the host's close ends goes: main's "closing the connection while a request awaits its answer" pins the same outcome, and stop-ends-awareness-drains pins its words. - test/mcp-server-ownership: main's two tests of the gate's recycle (an accounting failure recycling a server whose child can't be reaped, with and without reconnect) call installMcplDataPlaneGate and failMcplDataPlaneGate, which anima-research#250 removes, so they go with the gate. The close for good they lead to is still pinned for a runtime connect's failure, in mcpl-runtime-connect-failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
start()andaddModule()made while astop()is still running are now refused.start()throws andaddModule()rejects, each with<call> refused: the framework is stopping. This is the in-flight half of #315, as proposed in this comment there. It leaves #315's open question, whether astart()after a settledstop()should begin a new lifecycle, to be answered either way.Problem
stop()setsrunningto false before its first await, so astart()made while it runs passesstart()'srunningcheck.resetCompletedStop()clears a stop only once it has completed, sostart()then setsrunning, reopens provider admission and starts a second event loop under the stop still running. What follows depends on how far the stop has got. Onmainatc0155d3(Linux, Node 24.22.0), each case on its own framework over a store it owns:The stop hasn't yet seen its loop end. It waits on the new loop instead.
start(), withrunningtrue. It resolved only oncerunningwas set false by hand.stop(1000), it rejected after 1,007 ms withShutdown timed out after 1000 ms; pending: event loop, andrunningstayed true.The stop is past that wait, here while a module's
stop()was still running.stop()returned the same settled promise, and the loop ran on.start()changed nothing, sincerunningwas true.Neither
stop()norstart()could end that lifecycle.addModule()goes through the sameresetCompletedStop().stop(), the module was started and then stopped by the stop'sstopAll().stop()was still running, afterstopAll()had taken its list, the module was started and never stopped. It stayed registered after the stop settled, over the closed store.In connectome-host at
eb6f008,switchSession(src/index.ts:1333) awaitsstop()and creates a new framework rather than starting the stopped one. We found no call there that starts a framework while it stops, or adds a module to one.Changes
resetCompletedStop(), whichstart()andaddModule()both call before they begin any work, refuses while a stop is in flight, that is, whilestopPromiseis set and the stop hasn't completed. Its existing check comes first, so a stop that failed after closing an owned store is still refused asCannot start a new lifecycle: owned store is closed. A stop that failed with its store open has already clearedstopPromise, as before.quiesce refused: the framework is stopping).start()passesresetCompletedStop()before itsrunningcheck.stop()claims its attempt before it clearsrunning, so astart()made in the stop's first synchronous steps, such as from the push coalescer'ssuspend(), is refused too. On a running framework with no stop,start()still does nothing.start()made during a stop now say it's refused: three instopOwned(), the one onguardSpillsClosedFor, one insrc/gate/event-gate.ts, and a sentence indocs/tool-result-guard.md. The docs ofstart(),stop()andaddModule()say so too.stop()'s doc said astart()could be made "meanwhile" beside a loop that a timed-outstop()left in a call. It meant after that stop has failed, which is still allowed, and now says so.What each part is for, and what we left out
resetCompletedStop(). It's one condition and one throw in the method both entries already pass through before they begin a lifecycle's work, so a third entry added there would get it too. Astop()that captured, at entry, the lifecycle it stops would end the first case's wait. The second case shows what that leaves: the stop goes on to stop modules and close an owned store under the new lifecycle, and its settled promise answers every laterstop(). Astart()that waited for the stop would have to become async, and would then begin the restart start() after a completed stop() reuses an instance whose queue, coalescer and watchdog stay stopped #315 asks about.start()silently did nothing would go on as if the framework were running. The operator entry points throw too.start()'srunningcheck. Without it, astart()made before the stop clearedrunningreturned silently. It's the order of two statements.addModule(). It shares the method and the window. Without it, the orphaned module above remains.resetCompletedStop(), typed to the two callers.start()andaddModule()after a completed or failedstop()behave as before, which is start() after a completed stop() reuses an instance whose queue, coalescer and watchdog stay stopped #315's open question.Tests
test/start-during-stop.test.tsis new: six tests, each on its own framework.start()right afterstop(): refused, and the loop had ended when the stop resolved.start()in the stop's first synchronous steps, made by a stand-in push coalescer whosesuspend()calls it: refused.start()on a running framework: no second loop.start()while a module'sstop()runs: refused, the loop had ended when the stop resolved, and the store it owns is closed.addModule()while a module'sstop()runs: rejected, and its module neither starts nor stays registered.stop()has settled, neitherstart()noraddModule()meets the stopping refusal. It asserts nothing about what they do then.main'ssrc/framework.ts, the four refusal tests fail, and the other two pass.start()'srunningcheck put back before the refusal: the first-steps test fails;runningcheck removed: the running-framework test fails;start()for both calls: theaddModule()test fails;addModule()not passing throughresetCompletedStop(): theaddModule()test fails;resetCompletedStop()swapped:shutdown-errors.test.ts's "real Chronicle consumed close failure remains terminal with its original error" fails. It already pins the owned-store refusal after a failed stop.stop-with-work-in-flight.test.tsandshutdown-errors.test.tsfail, among them those that start the framework again after a timed-out stop.mainate5f50bfwith the twelve open pull requests that merged cleanly with it when these ran (fix(mcpl): reconcile channel lifecycle operations in order #210, fix(receipt): disabled-mode receipt says plain speech went to no channel, not that nothing was delivered #234, feat(routing): configurable homeChannel for no-trigger turn speech #244, fix(channels): the host posts nothing into a channel its server declared inbound-only #350, fix(gate): keep a gate.json that won't load, and reload gate files by their text #371, fix(gate): load gate.json and privileged-users files saved with a UTF-8 BOM (#395) #402, feat(workspace): port hands-mcp's edit planner, and add edit_many, several edits stored all or none #463, fix(code-execution)!: the runtime's channel moves to fds 3 and 4, and the host reads a script's stdout and stderr #495, fix(config)!: remove processLogging.broadcast and refusalHandling.announceHumanTurns #524, fix(tool-results): show each block of a content array, never the array's JSON #527, fix(workspace): the watcher holds its root on Linux, so an entry named like the root doesn't re-attach it #528 and fix(stop): a stop reaches a turn that has no live stream #529, in two trees), each tree typechecks and passesstart-during-stop.test.ts,shutdown-errors.test.tsandstop-with-work-in-flight.test.ts(74 of 74). fix(mcpl): fetch_reference's failures say what happened to the reference and what calling again can do #531, opened since, merges cleanly with this change too.Overlapping pull requests
addModule(), just before itsresetCompletedStop()call, which this PR changes. Each conflicts with this PR there, and keeping both resolves it: their check, thenresetCompletedStop('addModule()'). Both already conflict withmainelsewhere.maininsrc/framework.ts, and some of those conflicts now take in this PR's lines: in feat(framework): gated operator body changes with journaled recovery (stacked on #250; merge after it) #255,stop()'s doc,resetCompletedStop()and the comment on the stopping generation; in feat(focus): single-channel focus mode with held backlog, autoreply, and expiry #195, that comment; in feat(receipts): per-channel receipt clocks and fold receipts from confirmed provider rounds #253, the comment on provider admission near the end ofstopOwned(). This PR adds no conflict to them.Out of scope
Our internal review found three things outside what this PR changes, which it leaves as they are:
stop()that failed with its store open,start()goes ahead, as before, and by our reading so doesaddModule(). By our reading,leftRunning, which a laterstop()waits for, holds a stream or an event loop that outlasted the failed stop's wait, but not a module'sstop(). That's the settled-stop side of start() after a completed stop() reuses an instance whose queue, coalescer and watchdog stay stopped #315.runUntilIdle()drives the event loop with no check of the stop's state.runEphemeralToCompletion()admits a run byproviderAdmissionClosed, whichstop()sets only after its first synchronous steps.Not verified
stop()hasn't settled. Such a call now gets an error where it got the behaviour above.changelog.d/start-during-stop.fixed.mdWritten by Abigail-2459 (Claude Opus 5.5), contributing through nissa-seru. Ada-1017 (Claude Opus 5.5) agreed the design. Duncan-2410 (Claude Opus 5.5) reviewed the change, and it had an internal review before publication.
Description checked and posted by Lottie-2661 (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.