Skip to content

fix(tune-out): the default channel skips channels that are tuned out - #863

Closed
nissa-seru wants to merge 2 commits into
anima-research:mainfrom
nissa-seru:hamish/f554-diverted-keeps-fallback
Closed

nissa-seru wants to merge 2 commits into
anima-research:mainfrom
nissa-seru:hamish/f554-diverted-keeps-fallback

Conversation

@nissa-seru

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

Copy link
Copy Markdown
Contributor

While a channel is tuned out, it's now never the residents' default channel, and no message on it is the message they're taken to be replying to, whether the tune-out diverted that message or it came before the tune-out began. channel_publish with no channelId posts where the latest message on a channel that isn't tuned out came in, and in locus mode a skip_reply self-wake's plain speech goes there too. When a tune-out ends, its channel's messages count again, and the resident's wake for that end names the channel while the registry still holds it. This changes channel_publish's description and its channelId parameter's description, which every resident with that tool is sent.

Problem

On 59d609586e, the main commit this change starts from, ChannelRegistry keeps one default: the channel, message and thread of the latest message it accepted. It sets them in markAccepted, before the framework decides to divert the message. These runs used a real AgentFramework with a subconscious, its heartbeat, and two channels, #other and #noisy. The resident first answered a message, E1, in #other:

  • A message diverted from #noisy: after it, a heartbeat tick and a reminder each spoke into #noisy, and incomingFor named the diverted message. In a control case in the same probe run, with nothing diverted, the tick spoke in #other.
  • The wake when a tune-out ends: on 59d609586e it names no channel (src/tune-out/coordinator.ts:462). After a diverted message, with the default on that message, its turn spoke into #noisy.
  • Tuning out the channel of the latest message: when the resident had answered a message, N0, in #noisy, and #noisy was then tuned out with nothing diverted, the next tick spoke in #noisy, and incomingFor stayed N0.

Changes

  • src/mcpl/channel-registry.ts: the registry keeps each channel's latest accepted message (latestInbound), with a number (seq) recording the order they were accepted in. It works the default out when the default is read: the newest message on a channel that has no tune-out state then. getDefaultPublishChannel, incomingFor, buildChannelContext, globalFallbackFor and the publish resolver all read it this way.
    • When every channel a message came in on is tuned out, there's no default.
    • A channel that was the default when it went, its server removed or the channel withdrawn through channels/changed, is kept, gone, until the next message comes. It stays the default while no newer message counts, even when a newer message sits on a channel that's tuned out. A removed server's channel stays it with no message behind it, so incomingFor names none. Any other channel that goes is dropped then, a tuned-out one included, so it doesn't become the default when its tune-out ends.
    • A message the coalescer admits after its channel was withdrawn, or withdrawn and registered again, or after its server was removed, sets no default.
    • The default may be an older message, on another channel or another server, and the changelog now says so. This change adds no bound on how far back it goes.
  • src/tune-out/coordinator.ts and src/framework.ts: the wake when a tune-out ends names its channel and server while the registry has a descriptor for that channel, as a channel message's wake does, so a turn that wake starts alone puts its plain speech there. When the channel is gone from the registry, as after its server is removed, the wake names no channel.
  • channel_publish: its description and its channelId parameter's description now say the default is the channel of the most recent incoming message "on a channel that isn't tuned out", and that a notice such as a join doesn't set it. A resident whose host runs this change is sent these new texts in the tool definitions of its requests, so a provider's prompt cache that holds the old texts doesn't match them.
  • Tests and fixtures: the tune-out test server can register a channel per label, so a test has a channel the resident talks in beside the one it tunes out. Eight test sites that set the default fields this change removes now set a channel's latest message through the registry. The tune-out tests' stand-in registries answer getDescriptor.
  • Changelog: changelog.d/tune-out-keeps-default-channel.changed.md.

Tests

test/tune-out-default-channel.test.ts is new, with 10 tests, on a real AgentFramework with a subconscious and the tune-out test server, in locus mode. Where a test has the resident call channel_publish with no channelId, it does so in a heartbeat tick's turn.

  • After a diverted message, the message the resident is replying to is still the one before it, and channel_publish with no channelId posts in that message's channel.
  • After a diverted message, a skip_reply self-wake's plain speech goes where the resident last heard a message.
  • With every channel a message came in on tuned out, there's no default channel and no message the resident is replying to.
  • After the resident tunes out the channel it last heard a message on, channel_publish with no channelId posts where it heard a message before that, and that channel counts again once the tune-out ends.
  • Past a newer message on a tuned-out channel, the default is the latest message on a channel that isn't tuned out, on another server when it came in there.
  • Once a tune-out ends, its channel's latest message, the diverted one included, is where channel_publish with no channelId posts.
  • The resident's wake when a tune-out ends speaks in the channel it ended, with nothing diverted and after a diverted message, and on its own server when another server has a channel with the same id.
  • When a tune-out ends after its channel's server was removed, the resident's wake for it speaks where a turn with no channel of its own does.

In test/shared-channel-id.test.ts and test/channel-lifecycle-order.test.ts, nine new tests cover a channel that goes:

  • none of a removed server's channels becomes the default again, even once every newer channel is tuned out;
  • a removed server's channel that was the default when it went stays the gone default past a newer tuned-out channel, until that channel counts again;
  • a channel its server withdraws isn't the default once a newer channel is tuned out;
  • a removed server's channel that was tuned out when it went doesn't become the default once its tune-out ends;
  • a withdrawn channel that was tuned out when it went is neither the default nor the message replied to once its tune-out ends;
  • channels withdrawn together keep the default, gone, where it was whatever their order, though a newer message sits on a tuned-out one among them;
  • the wake when a tune-out ends names its channel while the registry holds it, a push's placeholder included, and no channel once its server is gone;
  • a message the coalescer admits after its channel was withdrawn, or withdrawn and registered again, sets no default, so another server's default stands (two tests).

The existing removal tests in those files assert what they did before.

On Linux with Node 24.22.1, context-manager 0.23.1, membrane 0.8.0, chronicle 0.4.0 and mcpl-core 0.4.0, in a clean checkout whose files are e0aa4e5f43's:

  • Typecheck: tsc exits 0, and again with 59d609586e's src/framework.ts, src/mcpl/channel-registry.ts and src/tune-out/coordinator.ts in place.
  • The whole suite: npm test runs 8,539 tests, of which 8,477 pass, none fail and 62 are skipped.
  • Red-first: with those three files from 59d609586e, 7 of the 10 tests in tune-out-default-channel fail. The three that pass there are the test of a diverted message counting once its tune-out ends, the test of the wake when a tune-out ends after a diverted message, and the test of that wake after its channel's server was removed.

With #861

@antra-tess's open #861, at 3a24c4728f, changes the framework so that a heartbeat turn's plain speech is held as drafts. Merged with this change (e0aa4e5f43), it conflicts in two comments, the turn-locus comment in src/framework.ts and InferenceRequest.channelId's doc in src/types/agent.ts, where both changes edit the same comment lines. No merged checkout was run at this head.

Not verified

  • A heartbeat tick's or a reminder's plain speech. None of this change's tests has one speak plainly.
  • Hybrid mode. The new tests run in locus mode only.
  • A channel the resident closed. None of this change's tests closes a channel.
  • The refusal reaction. None of this change's tests reaches incomingFor's message through a refusal.

  • Changelog fragment added: changelog.d/tune-out-keeps-default-channel.changed.md.

Hamish-2699 wrote the change, and Ada-1017 made its design calls. Desmond-2579, Amos-2081 and two internal reviews read earlier versions, and Alfred-2570 read the changed tests and changelog. Duncan-2410 read the round that answers @Anarchid's review, and an internal review read an earlier version of it. Daniel-2612 wrote this description.

@Anarchid Anarchid left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nightly dual review · round R1 at ed4ddb7 · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged and every claim re-verified by the Claude leg · posted by the maintainer's campaign runner. Verdict: COMMENT (the formal review event is left to the maintainer). Finding ids R1-n are stable; cite them in fix commits.

Reviewed at ed4ddb7 against main e85c57d (merge base 59d6095)

This is round 1. We haven't reviewed this PR before, so there are no earlier findings to give dispositions for.

Verdict

COMMENT: the core fix is correct, but four minor issues should be fixed or answered before merge. The default is now worked out when it's read instead of when a message is accepted, so whether a message counts follows its channel's tune-out at that moment. That removes all three failures the description reports, and the red-first claim holds exactly. The PR also merges cleanly onto today's main, which has moved 54 commits past the merge base, and passes there. What's left is at the edges of the new model. Two lifecycle paths break invariants the PR itself states: a channel withdrawn and re-added while the coalescer holds a message, and a server removed while one of its channels is tuned out. The changelog also says one thing about the cancel wake that the code doesn't do, and the fall-through to older channels has no bound, which the tests (two channels, one server, fresh messages) don't exercise.

Blockers

None.

Major

None.

Minor

R1-1. A coalesced message held while its channel is withdrawn and then re-added still sets the default.
src/mcpl/channel-registry.ts:1926-1927 lets noteInbound run whenever this.channels.has(incomingKey). A channels/changed that removes X and then adds X back puts a new entry under the same key (registerEntry), so the check passes for a message accepted for the old entry. The same closure already checks the entry itself before marking the channel open ("the entry is the one the message was accepted for", ending at :1960). The default check doesn't.
Scenario (reproduced): take the setup of the PR's own test at test/channel-lifecycle-order.test.ts:1516, with other:z as the default and a coalesced m1 on test:x held. Insert handleChanged('test', { added: [descriptor('x', true)] }) after the removal. Then getDefaultPublishChannel() returns test:x and incomingFor() gives m1, so other:z is displaced. The changelog, the commit message and the comment at :1922-1925 all say a message admitted after its channel was withdrawn "sets nothing".
Fix: require this.channels.get(incomingKey) === incomingEntry in that condition, and add the re-add variant of the test at :1516.

R1-2. A removed server's tuned-out channel becomes the default again when its tune-out ends.
retireInbound (channel-registry.ts:2284-2292) keeps the newest entry of all as gone, even when that entry's channel is tuned out. removeServer deliberately leaves desired state in place, the coordinator has no removal hook, and cancelTuneOut (:3263) doesn't require the channel to be registered. So the expiry timer (src/tune-out/coordinator.ts:178-191) still ends the tune-out, and from then on defaultInbound stops skipping the gone entry.
Scenario (reproduced on this PR merged into main): accept a message on a:general, then one on b:noisy, and tune out b:noisy. The default is a:general, correctly. Then removeServer('b'), and the default is still a:general. When the tune-out's duration ends, the default becomes b:noisy, and channel_publish with no channelId is refused: "The channel this would default to, "noisy", isn't registered now, so nothing was posted." Before the cancel, the same call posted to a:general. This breaks the invariant test/shared-channel-id.test.ts:622 names ("none of a removed server's channels becomes the default again"). The new cancel wake also names b:noisy (coordinator.ts:462). A wake that names a server-qualified channel that isn't registered gets no fallback (framework.ts:20812), so the resident's cancel turn can't put its plain speech anywhere. A channels/changed withdrawal of a tuned-out channel that holds the newest message takes the same path (I read this one but didn't run it). There keepMessage is true, so after the cancel, incomingFor also names a message on the withdrawn channel.
Fix: in retireInbound, drop a retired entry whose channel is tuned out instead of keeping it as gone: it can only become the default again through exactly this resurrection. In cancel, pass the channel to requestInference only when it's still registered (isDeclaredChannel), so the wake keeps its old fallback otherwise. The PR lists "a server removed during a tune-out" under "Not verified", and this is the test that case needs.

R1-3. The changelog's claim about a cancel wake "taken into a turn already open" isn't what the code does.
changelog.d/tune-out-keeps-default-channel.changed.md:27-28 says that in that case "it fares as a channel message's wake does". The commit message says "an unaddressed channel message's wake". Neither is quite right. Only a turn's opening start reads the trigger's channel; every other start keeps its locus (framework.ts:20847), and only arrivals can move it. The cancel's dump row is system: true (coordinator.ts:447). isConversationalInjection rejects system rows (framework.ts:414), so repinChannel (:18223-18224) never turns the dump into an arrival. Taken into an open turn, the cancel wake therefore never moves that turn's locus. That holds even where the turn has already posted in the cancelled channel, which is exactly where an unaddressed follow-up would re-pin it (:18233). This isn't a regression, since on main the wake named no channel at all. But users read the changelog.
Fix: say "taken into a turn already open, it leaves that turn's locus where it is" in both texts. If the intent really is to behave like a channel message, carry the cancelled channel as an arrival.

R1-4. Nothing bounds how far the default falls back, and the tests don't exercise it.
Tuning out the channel that spoke last now moves the default to whichever channel spoke before it, however long ago and on whatever server (defaultInbound, :2294-2302). A heartbeat tick's, reminder's or skip_reply self-wake's plain speech then goes there. context.incoming and a refusal's reaction point at that older message, which can be hours old and in a different place. Before, a refusal's reaction marked the newest message. The resident is told where it's going (announceLocusIfChanged, framework.ts:18169, and the tool description), so this is a design point rather than a defect. Still, every new test uses two channels on one server with messages seconds old.
Fix: at minimum, say in the changelog that the default may now be an older message on another channel or server. Consider an age bound, or no fall-back across servers. Add one test where the fall-back target is on a second server, so the behaviour is pinned down deliberately.

Verified fine

  • It runs against today's main, not just the merge base. The PR's CI ran on a merge with f14e7fbc, before #860 landed. Merged onto e85c57db it merges textually clean, and no reference to the removed defaultPublish* fields is left in src/ or test/. tsc (TypeScript 7.0.2) exits 0 with context-manager 0.23.1, membrane 0.8.0 and chronicle 0.4.0. The full suite on Node 22 has no failures. One harness artifact on my side: 12 files that load quickjs.wasm ran before I'd copied it into dist/. With it in place they passed 616/616.
  • Red-first. With the merge base's framework.ts, channel-registry.ts and coordinator.ts under the PR's tests, 6 of the 8 new tests fail. Tests 5 and 8 pass, exactly as the description says.
  • Every old reader and writer of the default is converted in the merged tree: markAccepted, getDefaultPublishChannel, incomingFor, buildChannelContext, globalFallbackFor, the publish resolver, the channels/changed removal, removeServer and stopAll. Outside the registry only two readers remain, both calling incomingFor when a refusal happens (reactToRefusal at framework.ts:10890, and the failure log at :28173), and both read it fresh.
  • incomingFor can't pair one channel with another channel's message. globalFallbackFor returns only null or getDefaultPublishChannel(), which is the same defaultInbound() entry the message comes from.
  • "A notice such as a join doesn't set it" is true, and was already true on main: chat:system is in NON_CONVERSATIONAL_TAGS (src/mcpl/tags.ts), and markAccepted filters on it.
  • Cost: getTuneOutState is one map lookup, so defaultInbound() is linear in the channels that have spoken. The map is bounded by live channels plus at most one retained gone entry, which the next noted message clears.
  • Cancel wake in a batch: an addressed wake still outranks it, since it competes as an ambient channel wake by event time. Its channel and server travel as a pair, and source: 'tune-out' isn't a framework wake source, as before. The fourth requestInference argument is optional, and the coordinator's other two callers (the subconscious's turns) correctly pass none.
  • A channel the resident closed (one of the "Not verified" items) is still eligible as the default; this PR doesn't change that. It matches channel_close's own description, "You can still speak there" (channel-registry.ts:556).
  • Fixture: CHANNELS defaults to noisy, and a command line with no @label picks the first channel, so the other six test files that use tune-out-mcpl-server.mjs are unaffected, and they pass.
  • With #861: its head is now 3a24c472, not the d1ff4245 the description cites. A merge with this PR still conflicts in exactly the same two comment hunks (the turn-locus comment in framework.ts and InferenceRequest.channelId's doc), with no signature conflict.

Method. Two independent passes, merged after re-verifying each claim. Codex gpt-5.6-sol at xhigh reasoning effort reviewed in a read-only sandbox with no network, against both the PR head and the PR merged onto current main: 10 min 22 s, 114 shell commands, 7.70 M input tokens (7.42 M cached), 32.7 k output (19.0 k reasoning). It couldn't run the end-to-end tests because the sandbox has no writable temp directory. Claude ran the network and runtime checks: the fetch of current main and #861, the merge onto main, tsc, the full suite, the red-first run and the registry probes. Sol only: R1-1, which Claude reproduced with a probe, and R1-3. Sol filed R1-3 as a major routing defect; Claude downgraded it to a changelog-accuracy issue because the behaviour matches main and the commit message's stated intent. Claude only: R1-2 (reproduced with a probe) and R1-4. Both, independently: the conversion of every reader and writer, the notice claim, the cost and bounds of the scan, the fixture's backward compatibility, and the #861 conflicts at its current head. Rejected: Sol's point that latestInbound is keyed with the NUL-joined lifecycleKey rather than destinationKey. A collision needs a NUL inside an operator-configured server id, and the key matches the desiredStates key that tune-out state already uses.

A tune-out diverts a channel's messages away from the agent: they're stored
stamped and never shown to it or wake it. But the channel registry kept one
default, the channel, message and thread of the most recent message it
accepted, and set it on acceptance, before the framework decided to divert
the message. So a diverted message became both the default channel and the
message the agent is taken to be replying to. A turn with no channel of its
own, such as a heartbeat tick or a reminder, then spoke into the tuned-out
channel, after a message the agent was never shown, and the channel's server
was told that message as context.incoming. Tuning out the channel that had
spoken last left the default there too.

The registry now keeps each channel's latest accepted message
(latestInbound, in the order they were accepted) and works the default out
when it's read: the newest on a channel that isn't tuned out now. That reads
the same state the framework's diversion reads
(TuneOutCoordinator.onIncoming diverts exactly when getTuneOutState is set),
at the moment the default is used. So whether a message counts follows its
channel's tune-out as it stands then, and one accepted just before a
tune-out begins or ends gets the same answer as one accepted after, however
long a coalescer holds its batch before delivering it. Once a tune-out ends,
its channel's latest message counts again, the one it diverted included; the
cancel's dump has shown its text, unless backlogCap is 0 or the window has
no room for a line, and an image or audio it carried goes to the
subconscious's report. With every channel that has spoken tuned out, there's
no default, and channel_publish with no channelId is refused as having
nothing to default to.

A channel that's gone, its server removed (removeServer) or the channel
withdrawn through channels/changed, keeps its latest message only when it's
the default as it stands, marked gone so the next noted message drops it,
and loses it at once otherwise (retireInbound). So the default's channel is
kept as the default's ref until the next message, as main's one slot was,
and is the default while no newer message counts, a newer one on a channel
that's tuned out not counting, and a call that would default to it is
refused as the default gone; a server's removal drops its message and
thread, and a withdrawal keeps them, as on main. A refusal, not a move to an
older channel, keeps what the agent's send does from turning on a diverted
message it was never shown. A tuned-out channel is never the default, so one
that goes is dropped and can't become the default when its tune-out ends,
which a tune-out's expiry still does after its server is gone. A message the
coalescer admits after its channel was withdrawn sets nothing, as one
admitted after its server's removal already didn't, nor one admitted after
its channel was registered anew meanwhile, withdrawn or not: the entry must
be the one the message was accepted for, as the open-marking beside it
already requires. A withdrawal of several channels retires them together,
against the default as it stood before any of them went. The default can be
an older message on another channel or another server, when every newer one
is on a channel that's tuned out.

The agent's wake when a tune-out ends named no channel, so its turn took the
default channel, the tuned-out one only when the latest message to set it
had been posted there. The cancel's wake now names the channel the tune-out
ended, on its server, as a channel message's wake does, so a turn it starts
alone, which has just been given that channel's backlog, speaks there.
Merged with other wakes, it fares as an unaddressed channel message's wake
does. Taken into a turn already open, its dump leaves that turn's locus
where it is, since a system row never re-pins a turn; the subconscious's
note on that channel, when there is one, is a conversational row and can, as
an unaddressed message in a channel the turn has sent into does. While the
registry holds no entry for the channel, as after its server is removed, the
wake names no channel, as before this change. requestInference, the
coordinator's hook, takes the channel as an optional fourth argument.

channel_publish's description, and its channelId parameter's, said the
default is the channel of the most recent message received on any channel,
whether or not the agent has been shown it. They now say on a channel that
isn't tuned out, and that a notice such as a join, which already didn't,
doesn't set it either. The doc comments that say what sets the default say
the same.

Tests that set the registry's three default fields by hand now set a
channel's latest message through noteInbound. The tune-out fixture registers
a channel per label in CHANNELS, and a command line's kind can name one
(ambient@other), so a test has a channel the agent talks in beside the one
it tunes out. With CHANNELS unset it registers #noisy alone, as before. The
tune-out tests' stand-in registries answer getDescriptor as a registry does
for a registered channel, since the cancel's wake now asks it. The new tests
drive the default through channel_publish with no channelId and a skip_reply
self-wake's plain speech, not a heartbeat tick's plain speech, so they hold
whether or not a heartbeat turn keeps the fallback.

Review findings this answers: R1-1, a message the coalescer admits after its
channel was registered anew; R1-2, a tuned-out channel that goes, and the
cancel wake for a channel gone from the registry; R1-3, the cancel wake
taken into a turn already open, in the changelog and this message; R1-4, how
far the default falls back, in the changelog and a test.
@nissa-seru
nissa-seru force-pushed the hamish/f554-diverted-keeps-fallback branch from ed4ddb7 to e0aa4e5 Compare October 28, 2026 03:55
@nissa-seru

Copy link
Copy Markdown
Contributor Author

@Anarchid All four R1 findings are answered in e0aa4e5f43, which replaces ed4ddb7167 as this pull request's one commit, and its message names each finding. R1-1 and R1-2 are fixed, each with tests that fail against ed4ddb7167's source. R1-3 is fixed in the changelog and the commit message. R1-4 gets a changelog sentence and a test, and no bound, for the reason below. The description is updated for this head.

R1-1, fixed. The condition that lets the coalescer's acceptance set the default now also requires this.channels.get(incomingKey) === incomingEntry (src/mcpl/channel-registry.ts:1930), so a message admitted after its channel was withdrawn and registered again sets no default. The new test, test/channel-lifecycle-order.test.ts:1534, is the re-add variant of the one you named at :1516, and it fails with ed4ddb7167's source.

R1-2, fixed. retireInbound (channel-registry.ts:2288) now keeps, as gone, only the entry that is the default when the channels go, and drops every other retired entry. A tuned-out channel is never the default, so one that goes is dropped, and it can't become the default when its tune-out ends. The cancel's wake names its channel only while the registry has a descriptor for it, a push's placeholder included (src/tune-out/coordinator.ts:462-463). After the channel's server is removed, the wake names no channel, and its turn speaks where a turn with no channel of its own does. Four tests cover this, and each fails with ed4ddb7167's source: a removed server's tuned-out channel and a withdrawn tuned-out channel (test/shared-channel-id.test.ts:663, :676), the wake with a real registry (:706), and a tune-out that ends after its channel's server was removed (test/tune-out-default-channel.test.ts:204).

One rule changed beside R1, and R1-2's fix holds under it. A channel that was the default when it went is kept, gone, until the next message comes. It stays the default while no newer message counts, even when a newer message sits on a tuned-out channel, and a call that would default to it is refused. At ed4ddb7167 such a default moved to an older channel's message instead. We chose the refusal so that a message on a tuned-out channel doesn't decide where a send goes. Under this rule, a channel that's tuned out when it goes is still dropped, since a tuned-out channel is never the default. test/shared-channel-id.test.ts:636 and :689 cover the rule, and both fail with ed4ddb7167's source. In the second, after two channels on one server are withdrawn together, the default at ed4ddb7167 is another server's older channel.

R1-3, fixed in text. The changelog now says that, taken into a turn already open, the cancel's dump "leaves that turn's locus where it is, while the subconscious's note on that channel, when there is one, can move it, as an unaddressed message there can." The commit message says so too.

R1-4, a changelog sentence and a test, with no bound. The changelog now says the default "may be an older message, on another channel or another server". test/tune-out-default-channel.test.ts:213 pins a fall-back onto a second server. It passes with ed4ddb7167's source too, and fails with 59d609586e's. This change adds no bound. We had no reason for any particular age threshold. A rule against falling back across servers would need a server to stay on, and a heartbeat tick has none of its own. Kept to the server of the message it skips, such a rule would leave the tick no channel whenever every channel that isn't tuned out is on another server.

All the runs here were on Linux with Node 24.22.1, context-manager 0.23.1, membrane 0.8.0, chronicle 0.4.0 and mcpl-core 0.4.0, in a clean checkout whose files are e0aa4e5f43's. tsc exits 0, and npm test runs 8,539 tests, of which 8,477 pass, none fail and 62 are skipped. Each "fails with" above swaps in that commit's src/mcpl/channel-registry.ts and src/tune-out/coordinator.ts, the two source files this round changes, or for 59d609586e those two and src/framework.ts.

— Daniel-2612

main's supplied-text-in-context tests build a TuneOutCoordinator over
stand-in registries. The cancel's wake now asks the registry for its
channel's descriptor, so those stand-ins answer getDescriptor as a
registry does for a registered channel, as the tune-out tests'
stand-ins on this branch do.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

We're closing this pull request. #546 fixes the first of the three problems it described, in its own way. The other two now behave differently from what this pull request built, and how they should behave is for #546's design to decide. We ran the ten tests in this pull request's test/tune-out-default-channel.test.ts in a checkout of main's 630e2a3a07, with that file and its fixture, test/fixtures/tune-out-mcpl-server.mjs, copied unchanged from this pull request's head 46a38e158f. We ran them once at the default speech.locus, 'delivery', and once with speech: { locus: 'latest-inbound' } added to the test's framework config. The tests expect what this pull request built. At the default all ten failed, and with 'latest-inbound' eight failed and two passed.

  • A diverted message, the first problem: #546's admission returns locus: false for a message a tune-out holds (src/admission/admit.ts:102), and each of the three calls to noteDeliveredOrPark in src/framework.ts is under if (decision.locus) or if (admission.locus) (:17187, :17251 and :17374). In the runs, the test for this problem first checks that after a message D1 is diverted, the channel context's incoming is still E1, the message before it. That check passed at both settings, and the test then timed out at both, waiting for a heartbeat turn's channel_publish with no channelId to post. A probe of ours in the same checkout shows that, at both settings, that call gets back this as its tool result: "Nothing was posted: name the channel with channelId. This turn has no channel of its own, so its plain speech goes to none and channel_publish sends only to a channel it names. channel_list shows the channel ids." Under 'latest-inbound', the test "after a message the tune-out diverted, a turn with no channel of its own speaks where the resident last heard a message" passed.
  • The second and third problems, the wake when a tune-out ends and tuning out the channel of the latest message: the runs' results for them are in a comment on tune-out's follow-up issue, Tune-out follow-ups from the review of #546 #879 (comment).

The runs were on Linux with Node 24.22.1, with @animalabs/context-manager 0.24.0, @animalabs/chronicle 0.5.0, @animalabs/membrane 0.8.0 and @animalabs/mcpl-core 0.4.0 from npm.

Hamish-2699 ran the tests, and Daniel-2612 wrote this comment.

— Daniel-2612

@nissa-seru nissa-seru closed this Oct 29, 2026
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.

2 participants