Skip to content

fix(mcpl): a redaction that can't read its config withholds the text - #542

Merged
nissa-seru merged 1 commit into
anima-research:mainfrom
nissa-seru:rupert/f240-redaction-fails-closed
Oct 19, 2026
Merged

nissa-seru merged 1 commit into
anima-research:mainfrom
nissa-seru:rupert/f240-redaction-fails-closed

Conversation

@nissa-seru

Copy link
Copy Markdown
Contributor

A redaction of a server's text no longer throws when it can't read the configuration it redacts with; it withholds the text instead. On a legacy MCPL connection, that throw rejected a reconnect with nothing to catch it, and two transport listeners threw it inside I/O callbacks.

Problem

A record of a server's text is redacted with the strings its configuration knows to be credentials (redactServerText, serverTextRedactor in src/mcpl/protocol-family.ts). Reading those strings can throw. A legacy stdio MCPL server's connection gets a copy of its configuration whose env is a getter. The getter derives the child's reaction baseline at each spawn, and it throws while the Discord awareness ledger can't be read, rather than give a partial baseline (getPlacedReactionBaseline). The redaction then throws too. Four of the legacy connection's records redact with that copy:

  • A reconnect attempt's failure (reconnectOnce). On main, I connected a legacy stdio server with reconnect: true, made the ledger unreadable, and ended the child. The reconnect's spawn read the getter and failed. Its catch redacted that failure with the same configuration, which threw the same error, and:
    • the attempt rejected with nothing to catch it. The process got unhandledRejection DiscordAwarenessAccountingError: … reaction-baseline ledger read …, which by Node's default ends the process;
    • the connection emitted no reconnect-failed, and the attempt stayed in flight. Two seconds after the ledger could be read again, the server was still down, with no attempt since;
    • the framework's stop() then rejected with the same error, since close() waits on that attempt.
  • The 'error' listener a handshake leaves once it has settled (logAfterSettled). I reached it as its test does, with a WebSocket server that refuses initialize and then sends a malformed frame, and a configuration whose env getter throws. The throw escaped as an uncaughtException, from inside the socket's I/O callback. The framework gives the getter only to stdio configurations, so I built that configuration for the probe.
  • The listener on a transport closed before it served, which has the same shape.
  • The initial connect's failure with reconnect: true (connectWithReconnect). It rejected the connect instead of returning the stub that retries. The framework reads the ledger itself before a legacy connect, so this one meets an unreadable ledger only if the ledger breaks after that read and before the connect's failure is recorded.

Changes

  • serverTextRedactor, which redactServerText uses, builds its patterns inside a try. When the configuration can't be read, that call returns WITHHELD_TEXT, [withheld: the server's configuration couldn't be read to redact this], in place of the text, and the next call reads the configuration again. Its doc and redactServerText's say so.
  • serverCredentialPatterns reads the configuration's url before the try that parses it, so a url that can't be read withholds the text too, rather than reading as absent.
  • The framework's every-server redactor (everyServerRedactor), for text that may come from any configured server, withholds the same way when any server's configuration can't be read.
  • The comment beside the framework's ledger read before a legacy connect says what a ledger that breaks after that read leads to (What a host notices).
  • A changelog fragment.

What each part is for

This change keeps a redaction of a server's text from throwing when it can't read the configuration it redacts with, so recording the text can't break what records it. Each part below is weighed by what it buys, what it costs to keep, and whether something simpler would do, leaving it out included.

  • The try in serverTextRedactor covers every caller at once: the four legacy records, and every other place that records a server's text. A try at each record would repeat it, and a record added later would need its own. Without the configuration the text can't be redacted, so withholding it is the one answer that leaks nothing.
  • Reading url before its parse's try keeps a configuration whose url can't be read from being redacted without the url's strings. That catch is for a malformed url, and it would treat an unreadable one as absent.
  • Reading again at the next call lets a connection's long-lived redactor recover once the configuration can be read, as it can once the ledger is. Keeping the failure would withhold that connection's records until a restart.
  • Fixed words, without the read error's text. That error is the host's, not the server's, and at the reconnect it's the whole line. Its cause still reaches the framework's trace, since reconnect-failed carries the message as raised, and the framework records it with the operator's own configuration, whose env is a plain object.
  • The every-server redactor reads the configurations the operator gave, so the framework's own getter can't make it throw, but a host can pass a configuration with a getter of its own. Three lines make it withhold there too.
  • The comment beside the ledger read keeps it from promising that nothing is left registered for an unreadable ledger, which holds only for a ledger that is unreadable at that moment.
  • The tests each fail without the piece they pin (Tests).

Nothing was cut.

What a host notices

Only where a configuration can't be read:

  • A legacy reconnect while the ledger can't be read fails as a reconnect. The connection emits reconnect-failed, and the attempts go on, so the server comes back once the ledger can be read. Its console line reads MCPL server "<id>" reconnect failed: [withheld: the server's configuration couldn't be read to redact this].
  • An error a legacy transport reports after its handshake settled, or while a transport that never served closes, is logged with those words, where it was thrown.
  • At startup, a legacy server with reconnect: true whose ledger breaks after the framework's read before its connect now leaves its stub retrying until the ledger reads again, and create() goes on. On main, the redaction's throw rejected that connect with the ledger's error, which ended startup. That read itself still ends startup on a ledger it can't read.
  • Where a host passes a configuration whose own getter throws, each place that records the server's text while it builds a result, an error or a log line now builds it with the withheld words. On main, the redaction's throw took its place:
    • a failed MCP or MCPL tool call answers with the withheld words as its error. On main, a call the model or a code_execution script made got no result, since its failure handler threw from a .catch() nothing awaits and the rejection went unhandled. By our reading, a script's call then waited out its tool-call timeout, 270 s by default. An in-process call (executeToolCall, or a module's ModuleContext.callTool) rejected its caller. A test runs each path.
    • startup's catch for a failed connect logs it, and startup goes on, where the throw ended it. A test runs it.
    • five more places, by our reading, without tests of their own. A modern request's failure keeps its error and its outcome. A failed modern connect whose cleanup also fails throws its own failure, where the getter's error replaced it. A modern initial connect with reconnect schedules its retry rather than rejecting. The modern connection's own 'error' listener logs, rather than throwing out of an SDK callback. And a degraded-features record logs, and the server's grant stands, where the expansion's catch took the throw for an unanswered policy request and threw again.
  • For agents, nothing changes where the configuration can be read. The four legacy records go to console.error, and the framework's redactions of text an agent reads use the operator's configurations or modern ones, neither of which has the framework's getter. By our reading, connectome-host passes configurations parsed from JSON, with no getter.

Tests

At this head, on main aae86b7f, on Linux, Node 24.22.0, with membrane 0.5.87, context-manager 0.14.1 and chronicle 0.4.0 installed (the repository commits no lockfile). The pull request's CI runs the whole suite on Node 20 and 24, on Ubuntu and macOS.

  • The touched test files pass: test/placed-reaction-baseline.test.ts 52 of 52, test/mcpl-transport-close.test.ts 24 of 24 and test/mcp-recorded-text.test.ts 14 of 14.
  • New, in test/placed-reaction-baseline.test.ts:
    • A legacy stdio server with reconnect: true, whose child ends while the ledger can't be read, logs its failed reconnect with the withheld words and reports reconnect-failed with the ledger's error as its cause. It comes back once the ledger can be read.
    • Startup with a server whose configuration has an env getter that throws goes on, and logs that server's failed connect with the withheld words.
  • New, in test/mcpl-transport-close.test.ts: two tests also run with a configuration whose env getter throws. The error a transport reports after a failed handshake settled, and those a transport reports while it closes having never served, are each logged with the withheld words, and nothing is thrown.
  • New, in test/mcp-recorded-text.test.ts:
    • A legacy initial connect with reconnect: true whose configuration can't be read returns the stub that retries, and logs its failure with the withheld words.
    • Under a configuration whose env getter throws, an in-process tool call that fails returns the withheld words as its error. A dispatched one gets them as its result's error.
    • redactServerText withholds the text while the configuration's env or url can't be read. A connection's redactor builds again at its next call, and redacts once it can. The every-server redactor withholds while one of its configurations can't be read, and redacts once it can.
  • Red-first. With main's src/mcpl/protocol-family.ts and src/framework.ts under these tests, the 8 new tests fail, and no other test. On main, the initial connect, the in-process call and startup each reject with the getter's own error, and the dispatched call's test fails on that error's unhandled rejection.
  • Removals. Each of the 4 pieces, taken out alone, fails at least one new test, and no other test in the three files. They are the try in serverTextRedactor, its building again at the next call, reading url before its parse's try, and the try in the every-server redactor. Each ran at this head.
  • Typecheck and build are clean.

Not verified

  • The two listeners with a stdio transport. Their tests reach them through a WebSocket server's malformed frame and through a transport the test controls, not through an error of a stdio transport's own.
  • The sites marked "by our reading" in What a host notices, which have no test of their own.

  • Changelog fragment added: changelog.d/redaction-fails-closed.fixed.md

Written by Rupert-2634 (Claude Opus 5.5), to a design Ada-1017 (Claude Opus 5.5) approved. Crispin-2406 (Claude Opus 5.5) found the two listeners while answering a review of #540. After a round of our internal review, Hilda-2681 and Barnaby-2576 (Claude Opus 5.5) reviewed it, and their points are in it. Barnaby-2576 approved this head, and Hilda-2681 its code, which this head leaves as she read it.

Description checked and posted by Fiona-2757 (Claude Opus 5.5).

🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's tooling, and posted from the shared nissa-seru account.

A record of a server's text is redacted with the strings its
configuration knows to be credentials. Reading them can throw: a legacy
stdio MCPL server's env is a getter that derives the child's reaction
baseline, and it throws while the Discord awareness ledger can't be
read. The redaction then threw too, from four of the legacy
connection's records:
- reconnectOnce's catch records the failed attempt with the same
  configuration whose read just failed it. The attempt rejected with
  nothing to catch it, which by Node's default ends the process. The
  connection made no further attempt, and its close() rejected with the
  ledger's error.
- The 'error' listener a handshake leaves once it has settled, and the
  one on a transport closed before it served, threw inside an I/O
  callback.
- connectWithReconnect's catch rejected the connect instead of returning
  the stub that retries.

serverTextRedactor, which redactServerText uses, now builds its
patterns inside a try. When the configuration can't be read, that call
returns WITHHELD_TEXT in place of the text, and the next call builds
again. serverCredentialPatterns reads the url before the try that
parses it, so an unreadable url is withheld too rather than taken for
none. The framework's every-server redactor withholds the same way. So
a redaction doesn't throw on a configuration it can't read, and a
reconnect made while the ledger can't be read fails as a reconnect and
is retried. The failure's cause still reaches the framework's trace
through the reconnect-failed event.

The framework reads the ledger before a legacy connect, which still
ends startup on a ledger it can't read. A ledger that breaks after that
read now leaves a reconnecting stub retrying until it reads again, as a
reconnect does, and the comment beside the read says so. Under a
configuration a host passes whose own getter throws, a failed tool call
now answers with WITHHELD_TEXT, where a call the model or a script made
got no result and an in-process call rejected, and startup's catch for
a failed connect logs and goes on, where it ended startup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Oct 19, 2026

Copy link
Copy Markdown

Required label not found on this PR.

@nissa-seru
nissa-seru merged commit fe82187 into anima-research:main Oct 19, 2026
5 of 6 checks passed
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 19, 2026
main now carries anima-research#542, whose redactions withhold the text, rather than
throw, when the configuration they redact with can't be read. anima-research#542 lands
ahead of this pull request, which takes its text where the two meet:
- protocol-family.ts merges without conflict: anima-research#542's withheld text and
  its read of the url sit beside this pull request's JSON forms and its
  escape boundary.
- The legacy error answer's log line redacts with the connection's
  configuration, so with anima-research#542 it withholds the message, where it threw,
  while a stdio server's reaction-baseline ledger can't be read. The
  call is still settled before the line is written.
- mcpl-agent-words' test of an unreadable configuration now asserts the
  withheld line beside the call settled with the server's words. A new
  test has the logger throw on that line, which leaves the call settled
  only because it was settled first, so that order still has a test that
  fails without it.

Receipts, on Linux. With this merge's src and tests, on Node 24.22.0:
the sixteen changed or most affected test files, anima-research#542's three among
them, pass, 400 tests. With src and changelog.d taken from main fe82187
and these tests, mcp-protocol-family can't load, since it imports
logValueRedacted, which main lacks, and 26 tests fail across six other
files. Each of 45 pieces, removed on its own at this merge, fails at
least one test. The whole suite at this merge, each test file in a
process of its own: the typecheck and the build are clean, and 240 files
and 4,640 tests (4,623 passing, 17 skipped, none failing) pass on Node
24.22.0, and the same on Node 20.20.2, with CI's stop pair passing on
both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 19, 2026
…hrow

The comment on handleResponse's error branch gave settling the call
before its log line the reason 2820a43 had: that line redacted with a
configuration whose env getter could throw. With anima-research#542, merged in at the
commit before this, that redaction withholds the message instead. The
comment now gives the reason that stands, and that mcpl-agent-words'
test of a logger that throws pins: the call's entry and timer are gone
by then, and a host's logger can throw.

This changes only that comment, so the receipts of the merge before it
hold for this tree's code.

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
…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>
tel-0s added a commit that referenced this pull request Oct 19, 2026
…llback, the all-except hold's channel included

What main brings since aae86b7 (0.22.3): #557, an ephemeral run's turn
locus, channel context and channel_publish default are its own channel or
nothing (`takesGlobalFallback`, `globalFallbackFor`;
`usedEphemeralAgentNames` becomes a Map of run | conversation); #559's
sized waits in the modern MCP outcome tests; #544's reconnect registration
and late-response rules; #547 and the F194 workspace spelling work; #402's
BOM-tolerant gate files; #540's agent words; #541/#542 publish and
redaction fixes.

Conflicts, all in src/mcpl/channel-registry.ts, resolved by composition
with the rule nissa-seru's review of #546 named: the all-except hold's
channel goes BEHIND the same check as the legacy fallback, so a subagent
whose run has no channel of its own never gets the focused channel either.

- buildChannelContext: main's `!== undefined` name keying and its
  `fallback` variable, where the fallback is `globalFallbackFor(agentName)`
  for a named conversation and the shared fallback for none.
- resolveLocus: `home ?? active ?? globalFallbackFor(conversationId)`.
  `globalFallbackFor` answers null for a conversation that takes no global
  fallback, else `sharedFallback()` = the focus channel, else
  `fallbackLocus()` (the latest delivered inbound under the legacy rule,
  otherwise null — Dendrite's `speech.locus`).
- channel_publish's default: main's caller-keyed form over the same
  `globalFallbackFor`, so a resident's publish with no channelId goes to the
  focus channel while a hold is open, else the legacy fallback; a subagent's
  is refused and names one. Before this merge the publish default on this
  branch skipped the focus channel; the three lookups now agree.

Two seams the build and tests found:

- framework.ts resumeAgent still called `.add` on the Set main made a Map;
  a resumed job is an ephemeral run, so it is `set(name, 'run')` and takes
  no fallback.
- A message delivered to a conversation fork (the `deliverTo` path and
  routeConversationIncoming) returned before `noteDelivered`, so on this
  branch it never became the latest delivered inbound — main's
  `markAccepted` recorded every accepted message, and #557's
  "a conversation fork keeps the fallback" test caught the gap. Both paths
  now record a delivered message as a resident's path does; held and
  dropped ones still never are.

Test adaptation: test/ephemeral-no-global-fallback.test.ts is about the
global fallback itself, so its fixture opts into `speech.locus:
'latest-inbound'`, as route-failure-targeting did at the third catch-up;
under Dendrite's `delivery` default a resident with no delivery has no
locus, which admission-e2e covers.

Composed rig: npm test 4862 tests, 4831 pass, 1 fail, 30 skipped; the one failure (host-quiesce's 2,101-message resume batch, a 60 s wait) was under three concurrent suites and passes alone, 48/48.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant