Skip to content

fix(create): a failed create() releases what it had acquired - #426

Merged
nissa-seru merged 4 commits into
anima-research:mainfrom
nissa-seru:fix/failed-create-releases
Oct 17, 2026
Merged

nissa-seru merged 4 commits into
anima-research:mainfrom
nissa-seru:fix/failed-create-releases

Conversation

@nissa-seru

@nissa-seru nissa-seru commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

A failed AgentFramework.create() now releases what it had acquired before its error reaches the caller: it stops the framework it had begun, as stop() does, and closes a store it opened. So the same process can call create() again on that storePath, which until now got Store is locked by another process until 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 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 (#389). The store stayed locked, so every later create() on that storePath in the same process got Store 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 failed create() never installs the SIGUSR2 listener, since nothing after its install can throw: checkToolPatterns() catches its own errors. And the failures I found after an MCPL child spawns, a DiscordAwarenessAccountingError on connect and a failed startup barrier, already close the children, in initializeMcpl()'s two catches, though with no deadline, and a close that failed there would replace create()'s own error.

Changes

  • create() opens the store as before, then runs the rest of its old body as createOn(), at its own indentation and unchanged but for one line, which records the framework as soon as it's constructed.
  • A failure in createOn() goes to releaseFailedCreate(), which:
    • stops the framework, if one was built, with stop() itself, so that whatever stop() comes to release, a failed create() releases too. stop() is already safe on a framework that never started.
    • gives that 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 failed create() never got one. Unbounded, a module whose stop() 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 failed create() never returns its framework for anyone to start().
    • closes a store 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 framework create() never returned. A store passed in as store is never closed.
    • rethrows create()'s own error unchanged when the release finishes cleanly. Otherwise it rejects with AggregateError([createError, ...releaseErrors]). Its message begins AgentFramework.create() failed: and says what didn't finish:
      • that part of what it started may still be running, when stop() failed or timed out before everything it waits on had stopped;
      • that finishing the stop failed, when 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 as stop() counts it: an MCPL server counts once its transport has sent the kill;
      • that its store may still be locked, when closing it failed.
  • initializeMcpl()'s two failure catches no longer close the MCPL servers themselves. The release closes them through stop(), within the deadline, with create()'s error first. On connectMcplServer()'s lazy path, the other caller, no server has connected when those catches run.
  • Keeping createOn()'s body at its old indentation keeps the diff to create()'s first lines, so open PRs that edit that body still merge cleanly with this one.
  • A module's stop() can now run before anything its host wires up after create() returns, since the release stops the framework before create() rejects. Module.stop()'s doc now says so. WorkspaceModule is wired that way: on a restart its start() holds the saved state until the host's initStore() makes the mounts, and its stop() 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's materialize no longer refused a branch that had diverged from the one last materialized to disk (from Greptile's review here). It now saves only once initStore() has run and the state it was handed has been applied, and until then the stored state stays as it was. That also covers an initStore() that throws after taking the store, which would otherwise leave a later stop() saving default pins over the saved ones. The other modules found that save state in stop(), Discord's and connectome-host's subscription GC, restore it at the top of start().

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.

  1. 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 a try where it stands would do the same, but would re-indent about 430 lines that several open pull requests edit.
  2. releaseFailedCreate() stops the framework with stop() itself, so a failed create() releases whatever stop() releases, now and as stop() changes. A separate cleanup list for modules, MCPL servers, the gate and the watchdog would have to be maintained alongside stop().
  3. The 30 s cleanup deadline (FAILED_CREATE_TEARDOWN_TIMEOUT_MS) applies separately to each of stop()'s two cleanup waits, not to the whole create() call. The failed call has returned no framework on which its caller could impose a deadline. The constant bounds these waits, including a module whose stop() remains unsettled; timing out stops waiting, not the module's work.
  4. The 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.
  5. stopFinishedTeardown distinguishes completion of stop()'s teardown stage from failure in a later stage. It records completion as stop() 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.
  6. initializeMcpl()'s two catches no longer close the servers themselves. That is a removal: cleanup goes through stop() with its deadline, and a cleanup failure is reported after create()'s own error.
  7. WorkspaceModule.stop() saves only once its saved state is applied, and Module.stop()'s doc says a stop() 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 possible stop() call.
  8. The tests pin each part. Each of ten removals fails at least one of them. The fragment: the repository asks for one.

Left out:

Tests

test/failed-create-releases.test.ts, eleven tests, each failing on main:

  • refused at the toolPrefix check: the module it started is stopped, create() rejects with its own error unwrapped, and a retry on the same storePath succeeds;
  • a module whose start() throws: every module started is stopped, that one included, and a retry succeeds;
  • a failure at the MCPL startup barrier after the server finished its handshake: create()'s own error, the server's own process gone (by the pid it recorded, within 10 s), and a retry succeeds;
  • refused at an invalid timeZone, before the framework exists: the store is closed, and a retry succeeds;
  • with a store passed in as store, through two kinds of failure: it stays open, the module is still stopped, and a retry on it succeeds;
  • a module whose stop() rejects: an AggregateError naming both, and the store is closed anyway, so a retry succeeds;
  • a store whose close() fails while a module's stop() also failed: the AggregateError carries all three, create()'s first;
  • a 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;
  • a module whose stop() never settles: still pending at 29.999 s (mocked timers), and rejected at 30 s with the message naming the timeout;
  • in a child process, a create() refused after the watchdog printed that it was armed: a 5 s busy stretch past its 2 s threshold isn't killed;
  • a session that materializes its workspace, then a restart refused at the toolPrefix check 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 before initStore(), and one stopped after an initStore() that threw once it had taken the store, neither saving over the stored state.

Checks

At this head, on main 24826254, on Linux with npm 11.19.1, and on main beside it:

Not verified

  • Bun. The suite runs on Node, and connectome-host runs this code under Bun.
  • macOS. CI runs it.
  • A context manager's background work. stop() doesn't wait for a context manager's speculative drain on any stop, so a failed create()'s release doesn't either. In context-manager 0.14.1 a drain starts when a message is added with autoTickOnNewMessage on, and its errors are caught and logged. No test covers a create() that fails while one runs.

Out of scope

  • stop() never removes the SIGUSR2 listener that create() installs, so every framework a process creates adds one, which keeps that framework reachable. Its fix is a change of its own.
  • Checking timeZone, toolResultInlineMaxChars and toolPrefix collisions before the store opens, as codeExecution already 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 no try/finally around 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(), the AggregateError, 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).

Pippa-2697 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>
@greptile-apps

greptile-apps Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5 Tier: apex

[Medium impact] The PR appears safe to merge; the earlier workspace state loss is fixed, and no new actionable issue was found.

Summary

Failed AgentFramework.create() calls now stop what they started and close stores they own.

  • Failed framework creation releases what it acquired before returning an error.
  • Workspace restarts keep saved pins until their state is applied.

Reviews (2) · Last reviewed commit: "fix(create): a failed restart leaves the..." · Reviewed by Greptile

Comment thread src/framework.ts
…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.
@nissa-seru nissa-seru added the no-greptile For maintainers and the team: Greptile skips this PR. Not for general use. label Oct 17, 2026
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 17, 2026
@nissa-seru
nissa-seru merged commit 61b6e0f into anima-research:main Oct 17, 2026
8 checks passed
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>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

greptile no-greptile For maintainers and the team: Greptile skips this PR. Not for general use.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A failed AgentFramework.create() leaves the store locked and started subsystems running

1 participant