Repository navigation
fix(create): a failed create() releases what it had acquired - #426
Merged
nissa-seru merged 4 commits intoOct 17, 2026
Merged
Conversation
added 3 commits
October 17, 2026 01:04
…esearch#389) create() opened the Chronicle store first, and went on to start modules, the liveness watchdog and the event gate and to connect MCPL servers. Steps after the store opens can still refuse, among them an invalid timeZone, an unreadable Discord awareness outbox, a module's start(), an out-of-range toolResultInlineMaxChars and an MCPL toolPrefix collision. Apart from closing MCPL servers on two paths (below), no failure path released anything. The store stayed locked, so a retry in the same process got "Store is locked by another process" until the process exited, and what had started kept running; the watchdog could later kill a process that carried on. create() now opens the store as before and runs the rest of its old body, unchanged and at its own indentation, as createOn(). The framework is named in `building` as soon as it is constructed. A failure in createOn() goes to releaseFailedCreate(), which: - stops that framework through stop() itself, so that whatever stop() learns to release later, a failed create() releases too (Ada-1017's call, room-203 #112494). stop() is already safe on a framework that never started; - gives that stop() a deadline, FAILED_CREATE_TEARDOWN_TIMEOUT_MS = 30 s per wait. stop() leaves its deadline to the caller, who owns the framework (ea7e043), but a failed create()'s caller never got one, and unbounded, a module whose stop() never settles would turn a failed boot that exits into one that hangs (Ada-1017, #112494); - closes a store create() opened even when part of that stop failed or timed out. stop() keeps its store open then so that it can be called again, but no one can call it on a framework create() never returned. A store the caller passed in as `store` is never closed; - rethrows create()'s own error unchanged when the release finishes cleanly, and otherwise rejects with AggregateError([createError, ...releaseErrors]), "AgentFramework.create() failed: <create's message>. Releasing what it had started didn't finish cleanly, so part of it may still be running: <...>". Keeping createOn()'s body at its old indentation keeps the diff to create()'s first lines, so open PRs that edit its body (anima-research#194, anima-research#371, anima-research#377, anima-research#378) still merge cleanly onto this. Two of anima-research#389's suspicions don't show on main 91e70ed: nothing after the SIGUSR2 listener's install can throw (checkToolPatterns catches its own errors), and the failures I found after an MCPL child spawns (a DiscordAwarenessAccountingError on connect, a failed startup barrier) already close the children, in initializeMcpl's two catches. Should a later step fail, stop() closes them. stop() itself never removes that listener; that is F47. test/failed-create-releases.test.ts, each test red on main for the reason it names: - refused at the toolPrefix check (anima-research#389's reproduction): the module it started is stopped, and a retry on the same storePath succeeds; - refused at an invalid timeZone, before the framework exists: the store is closed, and a retry succeeds; - a store passed in as `store` stays open through both kinds of failure, and the module is still stopped; - a module whose stop() rejects: an AggregateError naming both, and the store is closed anyway, so a retry succeeds; - a module whose stop() never settles: still pending at 29.999 s (mocked), rejected at 30 s with the double-fault message naming the timeout; - in a child process, a create() failing after the watchdog started leaves no watchdog: a 5 s busy stretch past its 2 s threshold isn't killed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…se (anima-research#389) From the review af389-failed-create (wf-1931, 20 issues, judged at 5a81605b): - initializeMcpl's two failure catches no longer close the MCPL servers themselves (I2, I19). They ran inside createOn(), before the release, with no deadline, and a closeAll() that rejected there replaced create()'s own error. create()'s release now closes every server through stop(), within FAILED_CREATE_TEARDOWN_TIMEOUT_MS, with create()'s error first. On connectMcplServer's lazy path the registry is new and empty when the barrier runs, so nothing there changes. - The rejection's message says what didn't finish (I14): a stop that failed or ran out of time says part of what create() started may still be running; a close that failed says the store may still be locked. - The docs, the changelog and the constant's comment name the waits the 30 s covers, stop()'s waits on operator operations and on modules and MCPL servers, instead of "each part of the release" (I1, I12), and say exactly when stop() keeps an owned store open (I15). Tests, in test/failed-create-releases.test.ts: - a clean release rejects with create()'s own error, unwrapped and with its exact message (I7); - a module whose start() throws: every module started is stopped, that one included, and a retry works (I5); - a create() that fails at the MCPL startup barrier after its server connected: create()'s own error, the child process gone, a retry works (I3; the awareness-barrier fixture with an injected reconcile failure); - a store whose close() fails during a release whose stop() also failed: the AggregateError carries all three, create()'s first (I11); - the watchdog test checks the watchdog's "armed" line and the refusal's message in the child's output, written to a file (I4). Not fixed here, as judged: stop()'s synchronous steps before its watchdog stop (I6) and its not waiting for Discord awareness drains (I10) are stop()'s on every path, for a change of its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s release (anima-research#389) The second internal review of the release (af389-failed-create-r1) found: - A stop that fails only after everything it waits on has stopped, in a guard's storage work, its final sync or closing the store, was reported as "Stopping what it had started didn't finish, so part of it may still be running", though everything it waits on had stopped then, as stop() counts it. stop() now records whether its teardown finished (stopFinishedTeardown), and the release then says "What it had started stopped, but finishing the stop failed". A new test has stop()'s own store close fail after a clean teardown. - create()'s doc and the changelog said only the modules' and MCPL servers' stops get 30 s. stop() gives the deadline to each of its two cleanup waits, its wait for an operator's operations already running first, so both now name both waits. - The MCPL test judged the server's exit by a process-wide count of child handles, for about 2 s. It now waits on the server's own process, by the pid the fixture records with its handshake, for up to 10 s. - A comment in connectMcplServerInternal still said an awareness accounting error goes to initializeMcpl, which tears down the other servers and aborts create(). That branch runs only for a runtime connect or restart, and since the first round initializeMcpl tears nothing down. It now says where the error goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[Medium impact] The PR appears safe to merge; the earlier workspace state loss is fixed, and no new actionable issue was found. SummaryFailed
Reviews (2) · Last reviewed commit: "fix(create): a failed restart leaves the..." · Reviewed by Greptile |
…ey were (anima-research#389) Greptile's review of anima-research#426 found that the release this PR adds reaches a module's stop() at a point no stop() reached before: after the module's start(), but before the host has the framework, so before anything the host wires up after create() returns. WorkspaceModule is wired that way. On a restart its start() decodes the saved state and holds it until initStore() makes the mounts, and connectome-host calls initStore() only after create() returns. Its stop() saved its mounts whenever it had a context, so a create() refused after the workspace module started (a later module's start(), a toolPrefix collision) saved { mounts: {} } over modules/workspace/state. The retry then restored no pin, and materialize no longer refused a branch that had diverged from the one last materialized to disk: anima-research#72's consequence, reached by a new path. WorkspaceModule.stop() now saves only once initStore() has run and the state start() was handed has been applied (savedState is cleared then). Before that the module has made no pass or write, so it has nothing of its own to save, and the stored state stays as it was. The second term also covers an initStore() that throws after taking the store and making the mounts, before it applies the saved state (at the disk agreement's open(), say), which would otherwise leave a later stop() saving default pins over the saved ones. Module.stop()'s doc now says it is also called when a create() that started the module fails, before any of the host's wiring. The other modules found saving state in stop() (Discord's here, and connectome-host's subscription GC) restore that state at the top of start(), so they save back what they were handed. Two tests: in failed-create-releases, a session that materializes, then a restart refused after the workspace module started, after which the stored state is as the session left it; in workspace-restore-order, a restart stopped before initStore() and one stopped after an initStore() that threw at the disk agreement's open(), neither of which saves. Each fails with the guard taken out, and the second fails with only its savedState term taken out.
This was referenced Oct 17, 2026
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 17, 2026
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 17, 2026
…'s default branch names, anima-research#426's failed-create release, anima-research#450's close reasons and anima-research#451's config checks Conflicts, and what each keeps: - src/framework.ts imports: this branch's McplRequestError beside anima-research#451's assertServerConfigBooleans. - initializeMcpl's connect catch: main's rethrow of a DiscordAwarenessAccountingError (anima-research#426), so an accounting failure during a startup connect ends startup, and create()'s release closes the servers. - initializeMcpl after its connects, and a runtime connect's tail: this branch's, with no data-plane gate. main changed only comments there, and anima-research#426 dropped the startup gate's closeAll in favour of create()'s release. - runRestartMcplServer: anima-research#451's config check before the disconnect, then this branch's journal read, for a server that derives the protective default only. - src/mcpl/server-connection.ts, a lost connection's rejection: this branch's McplRequestError ('no-response') with anima-research#450's closeReasonText. - test/fixtures/awareness-barrier-mcpl-server.mjs stays deleted, with the barrier it served. Carried from main: - anima-research#405's default branch names now pass over a name a suppression whose removals aren't recorded as finished claims, so a default name is never refused for a claim. While the journal can't be read, a claim counts for nothing in the choice, and checkDiscordAwarenessBranchName refuses the name with the journal's message. The helper names rollbacks, suppressions, turn undos and a host command's message undos. The offline recovery's default name follows the same rule once its journal is open; a dry run doesn't open the journal, so its default name passes over held branches only. - anima-research#426's test that a create() failing after its MCPL server connected closes the server: this branch's startup reconciles awareness before any MCPL server connects, so the test injects its failure into the tool discovery that follows the connects, and reads the handshake from the reregistering fixture's events. - anima-research#405's test that an explicit name stays the caller's: this branch refuses a known collision before anything is prepared, so the refusal it expects is "Rollback branch chosen already exists" (invalid), not the store's. New tests in operator-branch-names: a default rollback name a pending suppression claims takes the next free name; while the journal can't be read, a default name is refused with the journal's own message; an offline recovery's default name a pending suppression claims takes the next free name, while a dry run's passes over held branches only; and while the journal can't be read, an offline recovery's default name is refused with the journal's own message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 17, 2026
main gained, among others, anima-research#453 (F134: quiesce() writes its record first), anima-research#426 (a failed create() releases what it acquired) and c9d612e (the deferred queue's recovery drops entries the store can't take, one entry per id, and numbers seqs that aren't valid after the others). One conflict, in restorePersistedDeferredWrites. main changed the per-entry handling inside its catch around the whole restore, which logged any failure and restored nothing; this branch had replaced that catch with refusals for a failed read, a form that isn't a queue, and a failed check for writes that already landed. The resolution keeps this branch's refusals and takes main's per-entry handling whole between the form check and the landed check: wellFormed and its dropped-entry lines, one entry per id, the seq numbering with deferredSeq set from lastSeq, and the rewrite when entries were dropped, repeated or renumbered as well as when some had landed. Entries inside a queue the boot takes keep main's handling, as this change's rule says. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 17, 2026
With anima-research#426 in main, a failed create() stops the framework it built and closes a store it opened, so a test can boot again on that store. The helper no longer needs to find the framework through restoreUsageState and stop it: it asserts that the build fails with the expected message, and stops a build that succeeds, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru
pushed a commit
that referenced
this pull request
Oct 17, 2026
main moved from 119a862 to 3af13bf while this branch was in review. git merged src/modules/workspace/index.ts and test/workspace-three-way.test.ts on its own: main's changes there are #409's bounded reads, #426's stop() saving state only once initStore() has run, and #439's glob brace refusals, none of which touches the code this branch changes. One adaptation, to #439's new test: its shared check (test/workspace-glob-unclosed-brace.test.ts) expected the refusal as main's catch-all gave it, "Error: glob pattern …". This branch's catch-all answers an error's message without "Error: " (10bf065, pinned by "a refusal raised inside a tool reads without an "Error: " prefix", and said in the changelog's line on the workspace's refusals), so the check's expected text drops the prefix. Ogden-2688 found it by running the trial merge (room-203 #133814). No other test that uses the workspace expects that prefix.
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 18, 2026
Brings in main since 91e70ed, among it: the check of a server's boolean settings (anima-research#451), the release of a failed create() (anima-research#426), a broken stdin or stdout ending a stdio child's connection (anima-research#384), the stderr tail a failed handshake keeps (anima-research#399), a WebSocket close's reason (anima-research#450) and unbound references (anima-research#404). Conflicts, resolved by hand: - src/mcpl/server-registry.ts: this branch's structure (adding, register, holdsUnreapedChild) with anima-research#451's two reads, `config.reconnect === true` at the connect and `!connection.willReconnect` at the close. - src/framework.ts: - The startup catch and the startup barrier take main's form: an accounting failure or a failed barrier ends startup, and create()'s release closes every server through stop(), both engines' connections among them. This branch's closeMcpConnectionsAfterFailedStartup goes, with its test: stop() closes both engines' connections, each whatever the other's close did, and what it couldn't close is in create()'s rejection, where the old cleanup logged it. - The startup catch keeps this branch's redaction and willRetry, and takes main's stderr tail, redacted in its log line and its trace. - restartMcplServer checks the boolean settings beside this branch's checks, before the running server is torn down. - The legacy connect and reconnect failures' traces carry main's stderrTail, redacted, and the alert still leaves it out. - stop() resets main's stopFinishedTeardown, then ends the connects in flight. - handleManifestChanged reads its configuration and the announced domains before anything else. - src/mcpl/server-connection.ts: the handshake keeps this branch's protocolVersion and owner's close, and takes main's stderr tail on its transport failures. connectWithReconnect keeps this branch's throws, with `config.reconnect !== true`. The initial connect's and a reconnect attempt's log lines and events carry the stderr tail. A pending request's error is this branch's McplRequestError with main's closeReasonText. withStderr takes the server's redactor, so no log line prints the tail unredacted. - src/mcpl/types.ts: requestTimeoutMs's doc keeps both sides. - The tests: the reference fields' imports, and transport-close's `within` with main's bound. - changelog.d/stop-waits-for-operator-operations.fixed.md was released in 0.22.2. This branch's change to it, stop() ending a connect in flight, moves to a fragment of its own. Beyond the conflicts, so that the two sides agree: - anima-research#451's check skips inheritEnv for every network server, an http(s) one included, since only the stdio spawn reads it. - A script's view of an MCPL tool result registers its references bound to their server, as anima-research#404's lanes register the model's, so its expired reason is the stub's. The dispatch's fetch pass had bound each reference a script reads already. - anima-research#384's handling of a broken stdin or stdout (the child ended, its close naming the error, no 'error' event) replaces this branch's, so two of its tests now expect the close's reason. - Main's tests that wire a legacy connection pass the configuration this branch's wiring takes, and build a stand-in StdioTransport with its server id. The stderr tail's tests check its redaction in the log lines and the trace, and a new one checks it at startup. - The redaction changelog fragment names the stderr tail. Checks, on Linux: - Each of 8 pieces the merge adds, removed on its own, fails a test on Node 24.22.0: the stderr tail's redaction in the initial connect's, a reconnect's and the startup's log lines, in the startup trace, and in the legacy connect and reconnect traces; a script's reference binding; and the inheritEnv skip for an http(s) server. - The typecheck and the build pass, and every test file passes, each in a process of its own and unforced: 182 files and 2,940 tests on Node 24.22.0, and the same on Node 20.20.2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 18, 2026
nissa-seru
pushed a commit
to nissa-seru/agent-framework
that referenced
this pull request
Oct 18, 2026
…nima-research#426 does it F50's read of the conversation router's counters, before anything starts, closed a store create() had opened when the read failed, since a failed create() didn't release what it acquired. With anima-research#426 in main, a failed create() releases it: createOn's throw reaches create()'s catch, and releaseFailedCreate closes a store it opened (it checks isClosed, so the two didn't clash, but one close is the one to keep). The comment says the release closes it. The test that a create() failing on the counters closes the store it opened, so the next opens it, is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 task done
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.
A failed
AgentFramework.create()now releases what it had acquired before its error reaches the caller: it stops the framework it had begun, asstop()does, and closes a store it opened. So the same process can callcreate()again on thatstorePath, which until now gotStore is locked by another processuntil the process exited.!Fixes #389
We'll merge this not before 18:00 UTC on October 17, once our pre-merge checks pass on its merge with the then-current
main, unless you ask for changes before then.Problem
create()opens the Chronicle store first, and goes on to start modules, the event gate and the liveness watchdog and to connect MCPL servers. Steps after the store opens can still refuse, among them an invalidtimeZone, an unreadable Discord awareness outbox, a module'sstart(), an out-of-rangetoolResultInlineMaxCharsand an MCPLtoolPrefixcollision. Apart from closing MCPL servers on two paths (below), no failure path released anything (#389). The store stayed locked, so every latercreate()on thatstorePathin the same process gotStore is locked by another process. What had started kept running, the watchdog among it, which kills the whole process once the main thread stops beating for its threshold.Tests in this PR show each of these on
main: a module that is never stopped, the retry's lock error, and a child process that the leftover watchdog SIGKILLs during a busy stretch.Two of #389's suspicions don't show on
main. A failedcreate()never installs theSIGUSR2listener, since nothing after its install can throw:checkToolPatterns()catches its own errors. And the failures I found after an MCPL child spawns, aDiscordAwarenessAccountingErroron connect and a failed startup barrier, already close the children, ininitializeMcpl()'s two catches, though with no deadline, and a close that failed there would replacecreate()'s own error.Changes
create()opens the store as before, then runs the rest of its old body ascreateOn(), at its own indentation and unchanged but for one line, which records the framework as soon as it's constructed.createOn()goes toreleaseFailedCreate(), which:stop()itself, so that whateverstop()comes to release, a failedcreate()releases too.stop()is already safe on a framework that never started.stop()a deadline of 30 s (FAILED_CREATE_TEARDOWN_TIMEOUT_MS) for each of its two cleanup waits: on operator operations, then on modules and MCPL servers together.stop()leaves its deadline to its caller, who owns the framework, but the caller of a failedcreate()never got one. Unbounded, a module whosestop()never settles would turn a failed boot that exits into one that hangs.stop()'s other waits are on streams, the event loop, maintenance and guard annotations, which a framework nobody has started has none of, and a failedcreate()never returns its framework for anyone tostart().create()opened, even when that stop failed or timed out.stop()keeps an owned store open then, so that it can be called again, but no one can call it on a frameworkcreate()never returned. A store passed in asstoreis never closed.create()'s own error unchanged when the release finishes cleanly. Otherwise it rejects withAggregateError([createError, ...releaseErrors]). Its message beginsAgentFramework.create() failed:and says what didn't finish:stop()failed or timed out before everything it waits on had stopped;stop()failed only after that, in a guard's storage work, its final sync or closing the store.stop()now records whether it got that far (stopFinishedTeardown), and the release reads it. "Stopped" here is asstop()counts it: an MCPL server counts once its transport has sent the kill;initializeMcpl()'s two failure catches no longer close the MCPL servers themselves. The release closes them throughstop(), within the deadline, withcreate()'s error first. OnconnectMcplServer()'s lazy path, the other caller, no server has connected when those catches run.createOn()'s body at its old indentation keeps the diff tocreate()'s first lines, so open PRs that edit that body still merge cleanly with this one.stop()can now run before anything its host wires up aftercreate()returns, since the release stops the framework beforecreate()rejects.Module.stop()'s doc now says so.WorkspaceModuleis wired that way: on a restart itsstart()holds the saved state until the host'sinitStore()makes the mounts, and itsstop()saved its mounts whenever it had a context. So a restart refused after it started saved empty mounts over its saved pins, and the retry'smaterializeno longer refused a branch that had diverged from the one last materialized to disk (from Greptile's review here). It now saves only onceinitStore()has run and the state it was handed has been applied, and until then the stored state stays as it was. That also covers aninitStore()that throws after taking the store, which would otherwise leave a laterstop()saving default pins over the saved ones. The other modules found that save state instop(), Discord's and connectome-host's subscription GC, restore it at the top ofstart().What each part is for, and what was left out
The goal is to clean up resources acquired by a failed
create()and permit a retry in the same process. The earlier failure path could leave the store locked on retry and leave started resources, including the watchdog, active.createOn(),create()'s old body moved whole into a method at its own indentation, with one line that records the framework as soon as it exists, gives the release one place to catch a failure and the framework to stop. Wrapping the body in atrywhere it stands would do the same, but would re-indent about 430 lines that several open pull requests edit.releaseFailedCreate()stops the framework withstop()itself, so a failedcreate()releases whateverstop()releases, now and asstop()changes. A separate cleanup list for modules, MCPL servers, the gate and the watchdog would have to be maintained alongsidestop().FAILED_CREATE_TEARDOWN_TIMEOUT_MS) applies separately to each ofstop()'s two cleanup waits, not to the wholecreate()call. The failed call has returned no framework on which its caller could impose a deadline. The constant bounds these waits, including a module whosestop()remains unsettled; timing out stops waiting, not the module's work.AggregateError, and the message saying what didn't finish, tell the caller what a log line wouldn't: that part of the boot may still be running, or that the store may still be locked. When the release finishes cleanly,create()'s own error comes back unchanged, preserving the caller's existing error handling.stopFinishedTeardowndistinguishes completion ofstop()'s teardown stage from failure in a later stage. It records completion asstop()defines it, including an MCPL transport having sent its kill signal, rather than proving every underlying process has exited. The message can therefore distinguish incomplete teardown from a later flush or close failure.initializeMcpl()'s two catches no longer close the servers themselves. That is a removal: cleanup goes throughstop()with its deadline, and a cleanup failure is reported aftercreate()'s own error.WorkspaceModule.stop()saves only once its saved state is applied, andModule.stop()'s doc says astop()can now come before a host's wiring. The condition prevents a failed restart from overwriting the workspace's saved pins (Greptile's finding here). The doc line tells module authors about the earlier possiblestop()call.Left out:
stop()'s own ordering and its awareness drains, which are separate changes;SIGUSR2listener, which is its own pull request, fix(diagnostics): one SIGUSR2 listener per process, dumping each running framework #433.Tests
test/failed-create-releases.test.ts, eleven tests, each failing onmain:toolPrefixcheck: the module it started is stopped,create()rejects with its own error unwrapped, and a retry on the samestorePathsucceeds;start()throws: every module started is stopped, that one included, and a retry succeeds;create()'s own error, the server's own process gone (by the pid it recorded, within 10 s), and a retry succeeds;timeZone, before the framework exists: the store is closed, and a retry succeeds;store, through two kinds of failure: it stays open, the module is still stopped, and a retry on it succeeds;stop()rejects: anAggregateErrornaming both, and the store is closed anyway, so a retry succeeds;close()fails while a module'sstop()also failed: theAggregateErrorcarries all three,create()'s first;stop()that fails only in closing the store, after everything else stopped: the message says finishing the stop failed, not that anything may still be running, and the release's own close frees the store for a retry;stop()never settles: still pending at 29.999 s (mocked timers), and rejected at 30 s with the message naming the timeout;create()refused after the watchdog printed that it was armed: a 5 s busy stretch past its 2 s threshold isn't killed;toolPrefixcheck after the workspace module started: the stored workspace state is as the session left it.And in
test/workspace-restore-order.test.ts, one more: a restarted workspace stopped beforeinitStore(), and one stopped after aninitStore()that threw once it had taken the store, neither saving over the stored state.Checks
At this head, on
main24826254, on Linux with npm 11.19.1, and onmainbeside it:npm install,tsc --noEmit,npm run build,node --test --test-force-exit dist/test/*.test.js): the typecheck and build are clean.main's 2411 and the 12 above. 2414 of 2414 pass in the run. The other 9 are in a file that--test-force-exitcut short there (history-module-realstore), which passes in full, 28, when it runs alone without it.main, Node 24.22.0: 2411 of 2411 pass.stop-with-work-in-flight,stop-with-host-operationsandconversation-routing): 105 of 105.workspace-restore-order5 of 5.main's source and this test file and its fixture, all eleven fail, each for the reason it tests: a module never stopped,Store is locked by another processon the retry or when the workspace test reads the stored state, noAggregateError, a release that never reaches the module, and the child SIGKILLed by the leftover watchdog.createOn()returns (9), no store close (5), closing a store passed in asstore(1), noAggregateError(4), no deadline (1), a failed close left out of theAggregateError(1), a message that ignores whetherstop()finished its teardown (1), no guard inWorkspaceModule.stop()(1), and the guard without its applied-state term (1, theworkspace-restore-ordertest).create()'s body. fix(gate): persist sleep and self-wake intent across restarts; re-arm on backward clock step #194, fix(gate): keep a gate.json that won't load, and reload gate files by their text #371 and fix: the findings of #352's review #377 each merge cleanly with this one, and with each merged, this file and that PR's own tests pass: 39 of 39 with fix(gate): persist sleep and self-wake intent across restarts; re-arm on backward clock step #194, 41 of 42 with fix(gate): keep a gate.json that won't load, and reload gate files by their text #371 (its one skip needs a group the process is in besides its own), and 131 of 131 with fix: the findings of #352's review #377. fix(inference): a progress deadline on resident inference, retried when it expires #378 merges cleanly too, but its head doesn't build without membrane'sprogressTimeoutMs, which isn't released yet, as its own CI shows, so its tests didn't run with this one.Not verified
stop()doesn't wait for a context manager's speculative drain on any stop, so a failedcreate()'s release doesn't either. In context-manager 0.14.1 a drain starts when a message is added withautoTickOnNewMessageon, and its errors are caught and logged. No test covers acreate()that fails while one runs.Out of scope
stop()never removes theSIGUSR2listener thatcreate()installs, so every framework a process creates adds one, which keeps that framework reachable. Its fix is a change of its own.timeZone,toolResultInlineMaxCharsandtoolPrefixcollisions before the store opens, ascodeExecutionalready is, would spare those refusals starting and stopping modules. This change releases what any failure started, whatever refused.stop(), on every path, runs some synchronous steps before it stops the watchdog, with notry/finallyaround them, and doesn't wait for Discord awareness drains. Neither is this change's.Written by Pippa-2697 (Claude Opus 5.5), to design calls by Ada-1017 (Claude Opus 5.5): the release through
stop(), theAggregateError, closing the store after a failed stop, the 30 s bound, and the shape of the workspace guard. After each of two rounds of our internal review, and after the round for Greptile's finding here, Gordon-2414 (Claude Opus 5.5) reviewed it.Rent-section wording checked and edited by Fiona-2757 (GPT-6).