Repository navigation
Conversation
…ered at the start resumeActiveEpochs() walked listActive(), which read the tune-outs of the channels registered then, and create() calls it once. A channel its MCPL server registers only after the coordinator starts, as discord-mcpl does from a task that waits on the policy exchange or a grace timer, was left out: a tune-out of it whose end time passed while the process was down stayed on record with no dump and no end timer, and one whose end time was still ahead got no end timer, so it ended only when cancelled. Its held rows keep their tuneOut stamp, so nothing delivered them. listActive() now reads the registry's record (tuneOutsOnRecord), which the start's replay of the lifecycle log fills before any channel registers. A cancel needs nothing of the channel's registration: it reads the tune-out's state and the held rows, shows the channel by nameShown, which names an unregistered channel too, and records the end in the lifecycle log, as an end timer already does for a tune-out whose server was removed. So at the start a tune-out past its end time is cancelled, its dump stored and its end recorded, whether or not its channel is registered yet; one past its wake budget is cancelled too; and one still running has its cadence and end timer armed. The subconscious's window starts at it, and the start's pruning keeps its last look. enter() and enterAllExcept() read the same list for the window's start, so a later hold no longer moves the start past a tune-out whose channel isn't registered, such as one whose server was removed. The two lists an agent reads keep to the channels registered now, through listConnected(), the old walk: check's channels, and a cancel refusal's "Connected channels tuned out now", whose doc already says why (a removed server's tune-out is in neither that list nor channelsNamed). Four test files' stand-in registries answer tuneOutsOnRecord from the state they already keep. Tests, on macOS, Node 26.5.0, built, against the sibling chronicle and context-manager trees at their feat/presentation heads: test/tune-out-resume-late-channels.test.ts (new, 3 tests, on the real ChannelRegistry over a JsStore, with no channel registered at the start): a tune-out past its end time is cancelled at the start, its dump showing each held message once and its end on record for a new registry; one whose end time is ahead is in force a millisecond before it and ended at it, after its channel registers, its dump stored; and the subconscious's window starts at it at the start, at a later tune-out and at a later all-except hold, and its last look is kept. Red-first on main 48f26ce: all 3 fail (no dump; still in force at its end time; no anchor). Removals: each of the four readers of the record, given listConnected() on its own, fails at least one test: the start's walk fails all 3, enter()'s, enterAllExcept()'s and the pruning's the third. The cancel refusal given the record fails tune-out-messages' removed-server test; check() given it fails none, as no test checks a tune-out of an unregistered channel there. Not tested here: a tune-out of an unregistered channel past its wake budget at the start, which the same walk cancels. The 23 tune-out files (tune-out-*, all-except-*, admission-e2e, deferred-import-boot): 254 pass, 0 fail; on main, 251 pass in 22. The full suite, built and run as npm test runs it: 9499 tests, 9450 pass, 0 fail, 49 skipped; on main 48f26ce, 9496, 9447 pass, 0 fail, 49 skipped, the difference being the 3 new tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Automated review
Recommendation: merge and fast-follow. 2 medium severity findings can be fixed after the merge.
Reviewed commit 9704133 with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.
Does this change belong in the project?
It fits the project. This fits. It is a bug fix to the existing tune-out coordinator: tune-outs on record are now resumed at startup even when their channel registers later.
5 reasons with sources
It reads the registry's existing record API, stays inside the coordinator, and comes with the tests and changelog fragment the contributing guide asks for.
- It fixes the existing tune-out feature and adds no new scope. The coordinator already resumes active epochs at startup. Before this change,
resumeActiveEpochs()only looked at channels registered at that moment, so a tune-out whose channel registered later never got its end handled. The change keeps the same job and widens the list it reads. (src/tune-out/coordinator.ts) - It reuses a registry API that already exists instead of adding a second record of tune-outs.
ChannelRegistry.tuneOutsOnRecord()already exists, andsrc/framework.tsalready calls it. The coordinator now reads the registry's record of tune-outs instead of keeping its own, so the registry stays the owner of that state. (src/mcpl/channel-registry.ts) - What the agent sees stays limited to connected channels, as the existing docs say.
checkand the cancel refusal message now uselistConnected(), which is the old walk over registered channels. That keeps the documented promise that this text speaks only of connected channels. (src/tune-out/coordinator.ts (notTunedOut doc comment)) - It follows CONTRIBUTING's rules for behavior changes. It adds a
changelog.d/*.fixed.mdfragment and a new test file that the author says fails onmain. The description reports test counts as numbers and declares the AI authorship. (CONTRIBUTING.md (What a PR should contain; Changelog)) - The review's findings about removed servers and
listPolicyEpochsare defects for the code reviewer, not a conflict with any project design document. None of the documents I was pointed to say how a removed server's tune-out should affect the subconscious's window. The changelog fragment states the new behavior openly, so a maintainer can see it and decide. (README.md / CONTRIBUTING.md)
Findings
1 finding of medium or high severity, see the inline comment:
- Medium severity. A removed server's open-ended tune-out now pins the subconscious window, and the agent can neither see it nor cancel it.
1 finding about lines the diff does not show:
- Medium severity. listPolicyEpochs still lists only registered channels' tune-outs, while the coordinator now treats every recorded one as in force.
src/framework.ts:11504-11520
Details of this finding
- Medium severity,
src/framework.ts:11504-11520. listPolicyEpochs still lists only registered channels' tune-outs, while the coordinator now treats every recorded one as in force.listPolicyEpochs()describes itself as returning the "Policy epochs currently in force (tune-out holds)". It still walkschannelRegistry.listChannelsRaw(), the same walk this PR replaced in the coordinator'slistActive. After this change, a tune-out whose channel isn't registered yet, or whose server was removed, has its cadence and end timers armed at the start and holds the subconscious's window anchor, butlistPolicyEpochsleaves it out. The operator view therefore reports no epoch while one is active and will fire its end, dump its backlog and wake the resident. This parallel path should readtuneOutsOnRecord()too, or the difference should be stated in its doc.
1 more finding, less severe
- Low severity, test coverage,
test/tune-out-resume-late-channels.test.ts:98-117. No test checks that the cadence promised in the changelog is armed for a tune-out whose channel isn't registered. The changelog fragment says a still-running tune-out whose channel isn't registered "has its end timer and cadence armed" at the start. The second test only checks the end timer: it ticks toexpiresAtMsand asserts the cancel. It never makes a held message arrive afterhandleRegisterand advances bycadenceSeconds, so it does not check that the resumed cadence wakes the subconscious. Nothing in the suite would catch a regression whereresumeActiveEpochsskipsstartCadencefor such channels. Add a case that records a held message after registration and asserts the subconscious is invoked at the next cadence tick.
For a deeper review by several readers and a judge, which takes longer, a maintainer can comment /review deep.
| /** Every tune-out on record, its channel registered now or not: a channel | ||
| * its server registers only after the start, or whose server was removed, | ||
| * still holds what its tune-out diverted, and the tune-out's end time | ||
| * still releases it, which a cancel does without the channel. */ | ||
| private listActive(): Array<{ serverId: string; channelId: string; params: TuneOutParams }> { | ||
| return this.channelRegistry.tuneOutsOnRecord().map(({ serverId, channelId, params }) => ({ serverId, channelId, params })); | ||
| } |
There was a problem hiding this comment.
Medium severity. A removed server's open-ended tune-out now pins the subconscious window, and the agent can neither see it nor cancel it.
listActive() now returns every tune-out in tuneOutsOnRecord(), including those whose server was removed and never comes back. resumeActiveEpochs (line 459), enter (line 678) and enterAllExcept (line 1030) all take the oldest startedAtSequence from this list and pass it to setSubconsciousAnchor. A tune-out with no expiresAtMs on a removed server never ends: check() and notTunedOut now read listConnected(), so the agent is never shown it, and cancel_tuneout finds channels through channelsNamed, which only covers registered channels (the comment at lines 2111-2114 says so). The subconscious's window start therefore stays at that tune-out's start across every restart and every later hold, so the window keeps growing, and nothing an agent can do resets it. Before this change such a tune-out stopped holding the anchor. Either cap or expire unreachable tune-outs, or give the agent or operator a way to see and end them.
|
Take a tune-out whose end time passes while the process is down, while a later tune-out is still in force. With this pull request merged into #844's head, the subconscious's turns after the restart show the messages the first tune-out's dump left out. At this pull request's head they don't. At The case. Tune-out A on channel What the subconscious's turns after the restart show. These are the turns that carry A's cancel notice. A's dump shows only its newest message, and all three of A's messages are in the store after the restart.
An unheld message stored before both tune-outs started is in the store and in none of the subconscious's requests, at every head. This pull request merges into #844's head The runs were on Linux with Node 24.22.2 and membrane 0.8.0. The probe,
|
Found by nissa-seru's house reviewing #844 (the second of the two defects
mainhas too, in their comment there):resumeActiveEpochs()walkedlistActive(), which read only the channels registered whencreate()called it once. A channel its MCPL server registers later — as discord-mcpl does, from a task that waits on the policy exchange or a grace timer — kept a tune-out whose end time passed while the process was down on record with no dump and no end timer, its held rows undelivered; one whose end time was still ahead got no end timer.listActive()now reads the registry's record (tuneOutsOnRecord), filled by the start's replay of the lifecycle log before any channel registers. A cancel needs nothing of the registration (it reads the tune-out's state and the held rows, names the channel throughnameShown, records the end in the lifecycle log — as an end timer already does for a removed server's tune-out).enter()/enterAllExcept()and the start's pruning read the same list, so a later hold no longer moves the subconscious's window start past a tune-out whose channel isn't registered. The two lists an agent reads —check's channels and a refusal's "Connected channels tuned out now" — keep to registered channels throughlistConnected(), the old walk, as their docs say.Tests:
test/tune-out-resume-late-channels.test.ts(3, on the realChannelRegistryover aJsStore, no channel registered at the start): an expired tune-out is cancelled at the start with each held message shown once and its end on record for a fresh registry; a future end time gets its timer at the start and ends at that time after the channel registers; the window start stays at it through the resume, a laterenterand a later all-except hold. All three fail onmain; switching any one of the four record readers back tolistConnected()fails at least one. Four stand-in registries answertuneOutsOnRecord. Tune-out files 254 pass / 0 fail; full suite 9499 tests, 9450 pass, 0 fail, 49 platform skips (macOS).The first defect in that comment — the cancel recording the epoch's end before its dump — was already fixed on
mainby #909 (test/tune-out-cancel-order.test.tshas the scenario); the review had read4a45766f, before it.Written by an Opus agent under FableDendrite's direction; reviewed and pushed by FableDendrite.
🤖 Generated with Claude Code