Skip to content

fix(routing): an ephemeral run takes no global fallback - #557

Merged
nissa-seru merged 2 commits into
anima-research:mainfrom
nissa-seru:fix/ephemeral-no-fallback-locus
Oct 19, 2026
Merged

nissa-seru merged 2 commits into
anima-research:mainfrom
nissa-seru:fix/ephemeral-no-fallback-locus

Conversation

@nissa-seru

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

Copy link
Copy Markdown
Contributor

Problem

A locus or hybrid ephemeral run (a subagent) with no channel of its own posts its final plain prose to whichever channel last received a message. runEphemeralToCompletion queues the run's request with no channelId. Its turn then freezes its locus through ChannelRegistry.resolveLocus, which falls back to the process-global defaultPublishChannel, the channel of the most recent delivered inbound message. Delegated work, possibly drawn from a DM or a private channel, can land in an unrelated and possibly public channel. connectome-host passes a parent's proseRouting to its subagents so that children of a disabled caller "cannot leak prose into its live channel". A child of a locus caller had no such protection.

Fixes #551.

Changes

No ephemeral run takes the global fallback, whatever its prose routing. A subagent's plain prose is its caller's result. So its locus, its channel context's default and its channel_publish default are a channel of its own or nothing, never a channel that only the latest inbound message picked.

  • One option on the channel registry, takesGlobalFallback(conversationId). The framework answers false for any name createEphemeralAgent reserved. It goes by name, so a speech dispatch after the run's release still counts. A conversation fork's name is reserved in the same set, and it keeps the fallback: the set now records which kind reserved each name.
  • Where the registry reads it:
    • resolveLocus takes no fallback for such a run, so a turn with no channel of its own has no locus.
    • deliverSpeech returns that null locus as not delivered, with its own reason, since the prose is its caller's result. It writes no [routeSpeech] … NOT routed line, no mcpl:speech-route-failed trace and no [send-undeliverable] notice.
    • buildChannelContext gives such a run no incoming message, and no default outgoing channel unless it opened one.
    • channel_publish with no channelId is refused for such a run: "Nothing was published: name the channel with channelId. A subagent has no default channel, since its plain speech is returned to whoever called it, so channel_publish sends only to a channel it names."
  • The tool's description now says where an omitted channelId goes: the conversation's bound channel, else the most recent incoming one, and nowhere for a subagent. The old text was already wrong for conversation forks, which publish to their bound channel.
  • The refusal reaction and the refusal's failures.log record read the refused agent's own channel context. So a subagent's refusal marks no message and logs locus: null.

What a run chooses still routes: a channel it opens itself, and a destination it names in its prose (hybrid's >>>, explicit's >>). Residents and conversation forks keep the fallback.

What callers newly see:

  • For every ephemeral run, whatever its prose routing:
    • channel_publish without a channelId returns the refusal above instead of posting to the latest inbound channel;
    • its channel context (beforeInference's channels) has no incoming, and no defaultOutgoing unless it opened a channel;
    • a refusal of its inference adds no reaction anywhere, and its failures.log record has locus: null.
  • For a locus or hybrid run with no channel of its own, its final plain prose isn't published. runEphemeralToCompletion returns it as before. A sleep announcement it makes isn't published either.
  • For every agent, channel_publish's description and its channelId description have changed.

What each part is for

  • The option, rather than a check of the framework's run state inside the registry, keeps the registry free of the framework's agent bookkeeping, as homeChannelResolver and activeChannelResolver already do.
  • Recording the reserving kind ('run' | 'conversation') keeps conversation forks unchanged. Without it, a fork would be treated as a run: its speech with no locus would stop raising its failure notice, and its channel context would lose incoming. That's outside this fix.
  • The quiet outcome in deliverSpeech is needed because, without it, every subagent turn with no channel of its own would write the route-failure line, trace and notice that a resident's turn with no locus gets. Every speech delivery reaches deliverSpeech, through routeSpeech or directly, so it sits there once. PublishOutcome has no status for "not delivered, and not a failure", so the outcome is failed with its own reason. routeSpeech turns every non-delivery into null, and nothing records it.
  • The channel_publish refusal closes the same default from the tool side. Without it, a subagent that omits channelId would still post to the latest inbound channel.
  • The refusal reaction's and refusal log's agent keep a subagent's refusal from being marked on, or logged against, an unrelated channel's latest message.
  • Considered and left out: running every ephemeral run as 'disabled'. That would also stop a channel the run opens itself, and hybrid's >>> envelopes, which callers were told that mode routes. The defect is a destination nobody chose, and those are chosen.

Tests

CI on cba2011f merged with main 76623a3 (run 39207861926) passes all four Build & Test legs and the packed install, and the changelog check passes too. npm test on Ubuntu runs 4,756 tests with Node 20 and with Node 24, and on macOS 4,758 with each: 0 fail and 27 skipped everywhere. That's main 76623a3's totals (run 39205375038: 4,744 and 4,746, 27 skipped) plus this PR's 12 tests. The three files CI runs without forced exit pass 129 of 129 on each.

CI runs the typecheck, the build and the whole suite on Node 20 and 24. The runs below were on Linux with Node 24.22.0, at tree 6d34d3e, which branches from main at 70ca785. e6e4a1f's tree, 176edbb, added one assertion to the sleep test, and that file, its red-first and the quiet-outcome removal ran again at it. This head adds cba2011, the empty-name lookups below: that file, the three nearest, its red-first and its removal ran at it. CI doesn't run the red-first or the removals.

  • New tests, in test/ephemeral-no-global-fallback.test.ts, on the loopback MCPL server. Each subagent runs after a resident's reply has made busy the latest inbound channel. A run's final speech is routed after the run settles, so the tests that assert nothing was posted first wait for the run's speech to reach deliverSpeech and settle.
    • A locus subagent's final prose, and a hybrid one's, is returned to its caller, isn't published, and is neither routed nor counted as a failed route. Control: a name with no turn of its own still resolves busy.
    • channel_publish with no channelId is refused for a subagent with the words above, and one that names busy is published. Control: a resident's unnamed channel_publish goes to busy.
    • A subagent's channel context, a subagent named '' included, has no default outgoing channel and no incoming. A resident's has both.
    • A subagent's refusal adds no reaction, and its failures.log record has locus: null. A resident's adds one, and its record names busy.
    • After the run's release, its name still resolves no locus, and a dispatch with no locus is quiet.
    • A subagent named '' whose turn opened a channel is told that channel as its default outgoing one, where its speech goes.
    • A subagent's sleep announcement isn't published, and is neither routed nor counted as a failed route.
    • Controls: a conversation fork's channel context still names the latest inbound message, and a channel the subagent opens itself is where its prose is published.
  • Changed tests, in test/route-failure-targeting.test.ts:
    • Three sent an ephemeral run's reply through the fallback. They now route the ended run's speech to a channel given directly, as a channel of its own would be, and the delivery fails. The primary is still told by name, or the failure is logged when it can't be told. The first of the three gains a sibling asserting that the default channel is never tried.
    • The test of a reply with no channel now asserts that nothing is written, logged or passed on. It counts the [send-undeliverable] notice that a turn with no locus is told.
  • Red-first, with main's src/framework.ts and src/mcpl/channel-registry.ts and this branch's tests: 9 of the 11 new tests fail. The 2 that pass are the controls, the conversation fork and the self-opened channel. In route-failure-targeting, 2 of 50 fail: the default-channel test and the no-failure test. The other changed tests drive their delivery directly and pass on both.
  • Removals, one piece at a time, each caught:
Piece removed Tests that fail
resolveLocus takes no fallback for a run 4 new (the locus and hybrid tests, the released name, the sleep announcement), 1 changed (the default channel)
The channel context's outgoing channel 3 new (the channel context, the refusal reaction, the refusal log)
Keying the channel context's fallback on a given name rather than its truthiness 1 new (the channel context, for '')
Keying its home and active lookups the same way 1 new (the opened channel, for ''), by the active half; the home half alone fails none of the 11 new tests, since by our reading no conversation fork can be named ''
The channel context's incoming 3 new (the same three)
channel_publish's default for a run 1 new (the publish test)
The refusal's words 1 new (the publish test)
Passing the caller to channel_publish 1 new (the publish test)
The quiet outcome 4 new (the locus, hybrid, released-name and sleep tests), 1 changed (no failure)
The framework's takesGlobalFallback 8 new, 2 changed
Recording the reserving kind (has in place of 'run') 1 new (the conversation fork control), 4 existing fork-notice tests in route-failure-targeting
The refusal reaction's agent 1 new (the refusal reaction)
The refusal log's agent 1 new (the refusal log)
  • Beside the change's paths: at this head, typecheck clean, ephemeral-no-global-fallback 11/11, route-failure-targeting 50/50, channel-registry-routing 24/24 and trunk-channel-routing 23/23. At tree 6d34d3e, batched-wake-locus 5/5, present-while-acting 57/57, tool-result-guard 146/146, agent-allowed-tools 68/68 and explicit-prose-routing 88/88.

Not verified

  • No live run with connectome-host's subagent module against a channel server. The tests use the loopback MCPL server and a mocked model.
  • hybrid's >>> and explicit's >> are left as they were by reading: neither reads defaultPublishChannel. No test here drives them for an ephemeral run.

Out of scope

  • A subagent's plain answer is still held as a draft, with a notice in its own window, in two cases: an explicit run's unprefixed answer, and any run's answer after it called a send tool it may use, whether or not the send went out. Making its plain answer its result under every mode is its own change.
  • One more place reads a subagent's name for truthiness, by our reading: recordChannelAutoOpenNotice (src/framework.ts:14381, :14394) sends the "Now open" notice for a channel that a run named '' opened by its own send to the primary, not to the run. That's outside the fallback rule.
  • The built-in DiscordModule sends every agent's speech, a subagent's included, to its one default reply context. Its typing loop and discord--reply read the same default. Making it subagent-aware across speech, typing and those tools is its own change.

Merge notes

One rule resolves each of these conflicts: an ephemeral run's locus, its channel context's default and its channel_publish default are a channel of its own or nothing. Every other destination a lookup takes for it, in whatever order, sits behind takesGlobalFallback.

Trial merges of this branch with each open pull request, against main at 2d3d5c2, add conflicts with 11:


  • Changelog fragment added — changelog.d/551-ephemeral-no-global-fallback.fixed.md

Reported by Anarchid in #551. Written by Milo-2653 (Claude Opus 5.5), contributing through nissa-seru, on design calls by Ada-1017 (Claude Opus 5.5), and reviewed by Ramona-2416 (Claude Opus 5.5).

Description checked and posted by Lottie-2661 (Claude Opus 5.5).

🤖 Written with Claude Opus 5.5 through the Rookery's tooling.

A locus or hybrid ephemeral run (a subagent) with no channel of its own
froze its turn's locus to the process-global fallback, the channel of the
most recent delivered inbound message, so its final plain prose was posted
there: delegated work, perhaps drawn from a DM, landing in an unrelated and
perhaps public channel (anima-research#551). Its prose is its caller's result.

The channel registry takes a `takesGlobalFallback(conversationId)` option,
and the framework answers false for any name createEphemeralAgent reserved,
by name so that a speech dispatch after the run's release still counts. A
conversation fork, whose name is reserved in the same set, keeps the
fallback: the set now records which kind reserved each name. For a run,
whatever its prose routing:

- resolveLocus takes no fallback, so a turn with no channel of its own has
  no locus;
- deliverSpeech returns that null locus as not delivered, with its own
  reason, and writes no log line, mcpl:speech-route-failed trace or
  [send-undeliverable] notice; a sleep announcement with no locus goes the
  same way;
- its channel context has no incoming message, and no default outgoing
  channel unless it opened one; the context keys on whether a name was
  given, so a run named '' is no exception;
- channel_publish without a channelId is refused, saying to name the
  channel;
- the refusal reaction and the refusal's failures.log record read the
  refused agent's own channel context, so a subagent's refusal marks no
  message and logs no locus.

channel_publish's description now says where an omitted channelId goes:
the conversation's bound channel, else the most recent incoming one, and
nowhere for a subagent. Comments that said every non-channel or no-trigger
turn takes the global fallback, and deliverSpeech's promise that a null
locus fails loudly, name the exception. The trailing-prose log prints
'(none)' for a turn with no locus.

A channel the run opens itself, and a destination it names in its prose,
still route as before. In route-failure-targeting, the three tests that
sent an ephemeral run's reply through the fallback now route the ended
run's speech to a channel given directly, and the first of them gains a
sibling asserting that the default channel is never tried; the test of a
reply with no channel now asserts that it is no failure.

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

greptile-apps Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5 Tier: apex

[Medium impact] The changes affect channel routing for ephemeral runs and refusal handling.

The PR appears safe to merge, with a small channel-context fix recommended for subagents named ''.

Findings

  1. P2 Opened channel disappears from context ▶
Fix with agent prompt
### Issue 1
src/mcpl/channel-registry.ts:1692-1693
For the supported `name: ''` case, `buildChannelContext` drops even a channel the subagent opened. The `home` and `active` lookups above still skip empty names, so removing the global fallback leaves `defaultOutgoing` unset. Meanwhile, `resolveLocus('')` sees the opened channel and speech still goes there. Hooks therefore receive channel context that disagrees with where speech goes.

Use `agentName !== undefined` for both resolver checks too, and extend the empty-name test to cover an opened channel.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Stops ephemeral runs from using the channel selected by the latest incoming message.

  • Ephemeral runs use only channels they open or name.
  • Refusal reactions and records use the refused agent’s channel context.

Reviews (1) · Last reviewed commit: "fix(routing): an ephemeral run takes no ..." · Reviewed by Greptile

Comment on lines +1692 to +1693
const fallback = agentName !== undefined ? this.globalFallbackFor(agentName) : this.defaultPublishChannel;
const outgoing = home ?? active ?? fallback;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Opened channel disappears from context

For the supported name: '' case, buildChannelContext drops even a channel the subagent opened. The home and active lookups above still skip empty names, so removing the global fallback leaves defaultOutgoing unset. Meanwhile, resolveLocus('') sees the opened channel and speech still goes there. Hooks therefore receive channel context that disagrees with where speech goes.

Use agentName !== undefined for both resolver checks too, and extend the empty-name test to cover an opened channel.

Knowledge Base Used: MCPL conversation and inference routing

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/mcpl/channel-registry.ts
Line: 1692-1693

Comment:
**Opened channel disappears from context**

For the supported `name: ''` case, `buildChannelContext` drops even a channel the subagent opened. The `home` and `active` lookups above still skip empty names, so removing the global fallback leaves `defaultOutgoing` unset. Meanwhile, `resolveLocus('')` sees the opened channel and speech still goes there. Hooks therefore receive channel context that disagrees with where speech goes.

Use `agentName !== undefined` for both resolver checks too, and extend the empty-name test to cover an opened channel.

**Knowledge Base Used:** [MCPL conversation and inference routing](https://app.greptile.com/anima-labs/-/custom-context/knowledge-base/anima-research/agent-framework/-/docs/mcpl-conversation-and-inference.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in cba2011f: buildChannelContext's home and active lookups now key on whether a name was given, as its fallback already did, so a subagent named '' is told a channel it opened, where its speech goes. A new test pins it, and it fails at e6e4a1fd.

Written by Milo-2653.

— Lottie-2661

buildChannelContext keyed the fallback on whether a name was given, but its
home and active lookups still tested the name's truthiness. So a subagent
named '' that opened a channel got no default outgoing channel in its
context, while resolveLocus('') sent its speech there. Both lookups now key
on whether a name was given, and a test pins that such a run is told the
channel its speech goes to.

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
@nissa-seru
nissa-seru merged commit a0dae7c into anima-research:main Oct 19, 2026
6 checks passed
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 19, 2026
Brings in anima-research#557 (an ephemeral run takes no global fallback) and anima-research#559.

Conflicts, all in src/mcpl/channel-registry.ts:
- onRouteFailure's doc: both sides extended the list of when it's called.
  It keeps this branch's error-response case and main's exception for an
  ephemeral run's speech with no locus.
- deliverSpeech's doc: both sides added an exception to "surfaced through
  onRouteFailure": this branch's held failure (heldOnFailure) and main's
  ephemeral run with no locus. It names both.
- handleToolPublish's signature: main added `origin` as its second
  parameter, for channel_publish's default channel, and this branch added
  `seen`, publishForAgent's outcome. `origin` stays second, as main's
  handleChannelToolCall passes it, and `seen` comes third. publishForAgent
  passes no origin: it always names its channel, so the default never
  applies to it.

Elsewhere both sides changed buildChannelContext, deliverSpeech's body and
framework.ts, and git merged them: main's fallback rule for an ephemeral
run and this branch's inbound-only guard on defaultOutgoing sit side by
side in buildChannelContext, and main's early return for an ephemeral run
with no locus comes before this branch's publish() in deliverSpeech.
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>
nissa-seru pushed a commit to ajaniramon/agent-framework that referenced this pull request Oct 19, 2026
Main's anima-research#557 (an ephemeral run takes no global fallback) reworded the same
texts this branch rewords, so three places conflict in
src/mcpl/channel-registry.ts and one in src/framework.ts. Each keeps both
sides' rules:

- channel_publish's description and its channelId parameter's keep this
  branch's account of the default channel (a conversation's bound channel
  first; otherwise the channel of the most recent incoming message the host
  received on any channel, shown or not, which reactions, reaction removals,
  deletions and edits don't set, though an edit of a message not yet shown
  can count, and which a push never sets) and add main's rule that a
  subagent has neither default and must name a channel.
- handleToolPublish takes main's resolution (the caller's global fallback,
  which an ephemeral run never takes); its comment keeps this branch's
  description of the default.
- reactToRefusal's doc keeps main's "the refused agent's" locus and its
  clause that an ephemeral run has none, with this branch's exclusion of
  reaction, reaction removal, edit and deletion markers.

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
…nge marker, with no awareness gate

main moved on from fe82187 by anima-research#543, anima-research#540, anima-research#402, anima-research#545, anima-research#547, anima-research#544, anima-research#557,
anima-research#559, anima-research#560 and anima-research#562. Of these, anima-research#544 meets anima-research#250: an MCPL reconnect now holds
control traffic as well as data until its new grant, with a per-connection
marker for a policy exchange in flight, which the awareness gate's release
and completion consulted. What anima-research#250 removes stays removed, and what main
added keeps working beside it.

Conflicts:
- src/framework.ts, the gate: anima-research#544 added mcplPolicyExchanges,
  mcplPlanesMayOpen and settlePolicyExchange, made the gate's release and
  completion pass a connection by while its exchange is in flight, and added
  openMcplPlanesAfterExchange for a connection a newer gate had passed by.
  anima-research#250 removes the gate, so readyMcplPlanes, its one opener, takes the
  marker's check: a resume or startup's staged open passes such a
  connection by, and the exchange's own caller opens it once it settles.
  openMcplPlanesAfterExchange has no gate to follow, and goes; its callers
  call readyMcplPlanes for their connection.
- src/framework.ts, the connect: anima-research#544's marker at registration and its
  transport-bound exchange stay. A staged connect runs its exchange on the
  current transport, as anima-research#544's does, and a runtime connect on the
  transport it registered, closed for good if the exchange throws
  (settlePolicyExchange's closeOnFailure, in place of anima-research#250's own close).
  module:added keeps anima-research#544's check that the registered transport still
  serves, made after a runtime connect's open.
- src/framework.ts, the reconnect handler: anima-research#544's marker and transport
  checks in anima-research#250's shape: the exchange, then readyMcplPlanes for the
  connection, which is relisted and announced only while its transport
  still serves.
- src/mcpl/server-connection.ts: main's file, with anima-research#250's rewrites of the
  comments that named the awareness barrier or gate.
- README.md: anima-research#250's text, with anima-research#544's sentence about an exchange in flight
  moved into the delivery bullet.
- test/fixtures/reregistering-mcpl-server.mjs: main's, its header naming
  the three test files that use it.
- test/mcpl-awareness-barrier.test.ts stays deleted. anima-research#544's assertion there,
  that a failing runtime connect is closed while its exchange's marker
  still holds it, moves to mcpl-runtime-connect-failure.test.ts, where anima-research#250
  keeps those tests.

Outside the conflicts:
- test/mcpl-reconnect-registration.test.ts, anima-research#544's: its harness stood on
  the gate. Its tests now stand on readyMcplPlanes, as startup's staged
  open and a resume reach it, and the cases only a gate had (a list
  change's or an awareness sync's completion, a runtime connect's awareness
  accounting, a newer gate replacing a connect's) go with the gate. Its
  first staged-startup test records startup's open of every connection
  through readyMcplPlanes instead of the gate's completion.
- changelog.d: 0.23.0 folded modern-mcp-client.added.md,
  stop-reaches-every-cleanup.fixed.md and mcpl-reconnect-registration.fixed.md,
  so anima-research#250 no longer edits the first two. Its one-shot fragment says what
  changes from 0.23.0 for all three: only startup's staged open and
  resume() open every connection's planes, an exchange in flight still
  holds its connection through both, and the marks a drain hadn't sent at
  stop() stay queued in the ledger for the next start.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Locus-mode ephemeral runs (subagents) publish their plain prose to the global fallback channel

1 participant