Repository navigation
fix(tune-out): store the subconscious's last looks - #939
Conversation
The coordinator keyed a channel `${serverId}\0${channelId}` in its maps
and in the persistent subconscious's stored last looks
(tune-out/<primary>/last-looks). Chronicle's state write refuses an
object key that holds a NUL byte ("nul byte found in provided data"), so
every write of the looks failed. writeLooks logged the error and kept
the look in memory only, so a restart forgot every look, and the
subconscious's first cadence look after it counted every held message as
new.
A channel's key is now JSON.stringify([serverId, channelId]), parsed
back where the coordinator needs the pair, and the all-except scope's
timer key is 'all-except', which no channel's key, starting with '[',
can equal. No store holds looks under the old key, since every such
write was refused, so nothing reads it.
handOffKey, which keys the reader forks' hand-off bookkeeping in memory
and is never stored, is unchanged.
Test: coordinators over the same store, and over the store reopened from
disk, read back the looks another coordinator marked for three epochs,
one of them for the same channel id on two servers. Before this change
both read {}.
|
/review |
|
Automatic review of No findings. |
Anarchid
left a comment
There was a problem hiding this comment.
Nightly dual review · round R1 at 2ff23f9 · 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: CHANGES_REQUESTED (the formal review event is left to the maintainer). Finding ids R1-n are stable; cite them in fix commits.
Reviewed at 2ff23f9 against main 074ac60
Verdict: changes requested. The code change is right, and on current main it does what it says wherever a channel is registered before the boot reaches it. The new test skips the boot itself, which is where the other case breaks.
The key change is correct and complete. In the clean merge with today's main (296 commits past this PR's base), no NUL channel key and no '\0all-except' is left anywhere in src/, the new test is red on main's coordinator and green with the change, and the 48 tune-out-related test files pass. What's missing is the boot path. The new test reads lastLooks() from a coordinator that has never booted. A host's restart runs resumeActiveEpochs(), which ends in pruneLooks(). On main, that prune deletes the stored look of every per-channel tune-out whose channel isn't registered yet, and discord-mcpl registers its channels only after create() has run it. So for per-channel tune-outs on such a host, the changelog's promise holds only once #916 lands. Nothing in this PR says it depends on #916.
No earlier review of ours exists on this PR, so there are no dispositions to carry over.
Blockers
None.
Major
R1-1. On main, a real restart deletes the looks this change stores when the channel registers late. The new test can't see it because it never boots a coordinator.
resumeActiveEpochs()ends withthis.pruneLooks()(src/tune-out/coordinator.ts:515at this head).pruneLooks()keeps only the epochs inlistActive()(:1634), andlistActive()walkschannelRegistry.listChannelsRaw(), which lists registered channels only (:555–557).create()callsresumeActiveEpochs()once, right afterinitializeMcpl()(src/framework.ts:5267,:5283onmain). discord-mcpl registers its channels from a detached task that first waits for the host's policy exchange or a 20 s grace timer (discord-mcplmain,src/server.ts:1382–1397). #916's description describes the same order.- Probe on
main+ this PR, using the realChannelRegistryover aJsStore:- The channel registers,
enterTuneOutruns for epocheA, andmarkLookstores{"eA":{"[\"disc\",\"disc:guild:a\"]":42}}. - A fresh registry and coordinator are built over the same store.
resumeActiveEpochs()runs, and only then does the channel register.- The stored looks are now
{}, although the tune-out is still on record (getTuneOutState→eA).
- If the channel registers before
resumeActiveEpochs(), the look is kept. - With #916's diff applied on top (its
listActive()readstuneOutsOnRecord), the look is kept in both orders.
- The channel registers,
- Failure scenario: a host with a persistent subconscious and a discord-mcpl channel tuned out restarts. During the run its looks were stored. At boot they are written away before the channel registers, so the next look counts every held message as new, which is what #917 reports.
- Today this is masked: on
mainthat channel also gets no cadence timer after the restart (startCadenceis reached only from the resume loop,:459, and fromenter,:683), and that is #916's bug too. - Once #916 lands, it also fixes the prune. Until then, the changelog line "after a restart its next cadence look counts only the messages held since that look" is true for the all-except hold, whose epoch comes from its own stored record, and for channels registered before
create()callsresumeActiveEpochs(). It is not true for per-channel tune-outs on discord-mcpl.
- Today this is masked: on
- Fix:
- Say in the description that this depends on #916, and land it after #916 or together with it. Alternatively, carry the part of #916 that matters here:
pruneLooks()should take its active epochs from the tune-outs on record, not from the registered channels. - Extend the new test so the reading coordinator boots: call
resumeActiveEpochs()on the coordinator over the reopened store and only then readlastLooks(), with the epochs on record throughenterTuneOut/enterAllExceptrather than as bare ids.- As written, the test's epochs (
e-channel,e-all-except, …) are on record nowhere, so a boot would prune all three. That is why the test can only pass by skipping the boot.
- As written, the test's epochs (
- Run that boot once with the channel registered before
resumeActiveEpochs()and once registering after it. The second case is the one that needs #916.
- Say in the description that this depends on #916, and land it after #916 or together with it. Alternatively, carry the part of #916 that matters here:
Minor
R1-2. A look is stamped when the subconscious's turn is asked for, not when the turn runs. Now that stamps survive a restart, a restart in between skips that look for good.
invokeSubconscious()adds the notice, callsrequestInference(), thenmarkLook(), which is now a durable write (:1533–1538).- The framework's
requestInferencehook only pushes onto the in-memorypendingRequests(src/framework.ts:37127–37129onmain). Nothing at boot asks for the subconscious's turn again. main's quiesce parks wakes inpendingRequestsuntil resume ("Wakes park in pendingRequests … and fire at resume",src/framework.ts:8860). So a quiesce-then-stop deploy that catches a cadence look in between drops the turn and keeps the stamp.- Failure scenario: a cadence look fires, then the host stops before the subconscious's turn runs. After the restart, those held messages sit at or below the stored stamp, so they count 0. The channel gets no further look until new traffic arrives there or the hold ends.
- Before this change the restart forgot the stamp and over-counted: the first look counted everything, which is the safe direction. The reader-fork design picks that side on purpose ("which is the right side to err on",
:355–357). - The existing test at
test/tune-out-reader-queued.test.ts:537pins "a quiet period after it has none", so durable stamps are the intended design. This is the decision the description asks @antra-tess to make, and the window above is the part of it that isn't visible from the PR. - Fix: either:
- stamp the look when the subconscious's turn starts, through a hook from the framework; or
- at boot, ask for the subconscious's turn again when its last tune-out notice has no turn after it; or
- accept the window and say so in the
lastLooksCachedoc comment (:342–346, which already says "the store's sequence when the look was asked for") and in the changelog.
R1-3. Two of the four rewritten channelOf() sites aren't exercised by any test.
- Mutation run on
main+ this PR, over the 48 tune-out-related test files (1,112 tests), puttingkey.split('\0')back at one site at a time:- the boot tally loop (
:512): 5 failures; - the all-except cadence loop (
:1099): 5 failures; cancelAllExcept's pending-wake sweep (:1198): 0 failures;- its tally sweep (
:1239): 0 failures.
- the boot tally loop (
- With the old split at those two sites,
getTuneOutState(<the JSON key>, undefined)returnsnull. Ending the all-except hold would then clear the pending wake and the unread tally of a channel whose own tune-out is still in force, and no test would notice. - These two sites were untested on
mainas well. This PR rewrote them by hand, and two open PRs touch the same code (see "Merge order" below). - Fix: add one test. Put a channel under its own tune-out with a pending wake and a non-zero tally while the all-except hold is in force, end the hold, and assert both survive.
- Optional, sturdier: keep the pair in the maps' values, or brand the key type, so a caller can't split it at all. A
split('\0') as [string, string]cast still compiles against the new key.
Nits
R1-4. The test's t.after closes only reopened (test/tune-out-last-looks-stored.test.ts:44–45). If an assertion before :65 fails, store is never closed before the rmSync. The sibling test/state-read-and-write-errors.test.ts:73–80 closes in a finally.
Merge order (not defects of this PR)
- #844 (head
ec41c176) now conflicts textually with this PR insrc/tune-out/coordinator.ts, on theSCOPE_KEYdeclaration, which #844 moved. The PR description'smerge-treesurvey predates that. Take'all-except'. Everything else in the merge resolves tochannelOf(). - #916 merges cleanly with this one. Its new test seeds looks under the old key (
{ eA: { [\disc\0${CHANNEL}`]: 8 } }) with the comment "the store refuses their keys, which hold a NUL". After this PR, that comment is false and the seeded key no longer matches a channel. The test still passes (32/32 across #916's, this PR's and the reader-queued file combined), because it only checks that the stored object is kept. Whichever PR lands second should switch the seed toJSON.stringify(['disc', CHANNEL])`.
Verified fine
- The defect: with
main's coordinator in place, the new test fails, and the log showsthe subconscious's last looks (tune-out/scout/last-looks) could not be stored: Error: nul byte found in provided data at position: 4four times. This is chronicle 0.5.1 as resolved bynpm install(no lockfile) under Node 22.22.1. - Completeness of the key change in the clean merge with
main: nosplit('\0'), NUL channel key or'\0all-except'is left insrc/.- The NUL keys that remain are
handOffKey(:404, and its prefix check indropHandOffs), which keys only the in-memoryreaderCursor/readerHandedWrites/readerReturned. - Test stand-ins that key their own maps with NUL (
test/tune-out-reader-queued.test.ts:63–65) don't depend on the coordinator's format.
- The NUL keys that remain are
- Where the refusal comes from: napi-rs (2.16.x) converts each property name of the
serde_json::Valueargument withCString::new(bindgen_runtime/js_values/object.rs,get_inner). That happens before chronicle'sset_state_json(src/napi.rs:1053atv0.5.1,:1046atv0.5.0) serializes anything. So the refusal is the same on 0.5.0 and 0.5.1, and the same under Bun, because the conversion runs in the native addon. - Old-format looks need no migration, as the description says. No release ever stored one:
v0.29.0has no last looks, and every later release takes a chronicle that refuses the key.ChannelRegistry's store is typedJsStore(src/mcpl/channel-registry.ts:995), andsrc/has no other store. - The new key:
JSON.stringifyescapes a NUL as\u0000, so the key can't contain a raw NUL whatever the ids. It always starts with[, so it can't equal'all-except'. - No API or trace exposes the key:
check'sheldmap is keyed byshown(), notkey(). - Sequence comparison across restarts:
StoredMessage.sequenceis chronicle's record sequence (context-manager'smessage-storesetssequence: record.sequence), the same counter asstore.currentSequence(), whichmarkLookstamps. Som.sequence > markstays valid across a restart. - Write cost: chronicle's
setStateJsonon a snapshot state appends the whole value each time, so every look rewrites the whole looks map. That is epochs × channels entries, pruned at epoch end and at boot, and small next to the subconscious turn each look asks for. - Release history in the description:
v0.29.0has no last looks (chronicle^0.4.0).v0.30.0(chronicle^0.5.0),v0.31.0andv0.32.0(latest, chronicle^0.5.1) all ship the NUL key. So every release with last looks is affected, not only 0.30.0. - Other coordinator records: the all-except record and the dispositions don't key by channel, so they aren't affected. The dispositions are keyed by the model's own note keys.
- Changelog fragment: the name matches
scripts/release-changelog.mjs's<slug>.<fixed>.mdpattern. - Tests on the merge with
main:tscbuilds, and the 48 test files whose source namesTuneOutCoordinator,tune_out,tuneOut,cancelTuneOutorall-exceptgive 1,112 pass / 0 fail.
Method. Two passes in parallel: Claude (Opus 5.5) did the cross-layer pass, and Codex gpt-5.6-sol at xhigh ran read-only over a worktree of this head with main as the base. Every Sol finding was re-checked against the source before it was adopted. Builds and tests ran on the clean merge of this head with main 074ac60b, with the published dependencies from npm. Sol ran for about 10 minutes: 113 shell commands, 4.72M input tokens (4.50M cached), 24k output tokens. A first launch stopped early on a local tooling prompt and was relaunched. Found by both: the key change is complete across the coordinator and its consumers, the NUL hand-off keys are never stored, the new test is not vacuous, and nothing public exposes the key format. Sol only: the napi-rs CString source of the refusal. Claude only: R1-1 to R1-4 and the merge-order notes. R1-1 rests on a probe over the real registry and store, R1-3 on a mutation run, and both on builds Sol's read-only sandbox couldn't do; it also couldn't run the native-store test there, since mkdtemp was refused. Rejected: Sol's one finding, that NUL-keyed looks from a NUL-tolerant store go unmigrated. No shipped store accepts those keys (see Verified fine). Sol had no network, so discord-mcpl's registration order, the open PRs and the release tags were checked on the Claude side.
At a start, resumeActiveEpochs() ends in pruneLooks(), which kept the stored last looks only of the epochs listActive() found, and listActive() walks the registered channels. A channel whose server registers it only after the resume had no epoch there yet, so the start wrote its looks away while its tune-out stayed on record. The start's own recount read the look before the prune, so the first start counted the channel's held messages right, and a later start, with no look since, counted them all as new. pruneLooks() now takes the active epochs from the tune-outs on record, channelRegistry.tuneOutsOnRecord(), whether or not their channels are registered yet, and the all-except hold's as before. Test: a coordinator marks looks for a tune-out entered through the registry and for the all-except hold; a new coordinator over the store reopened from disk runs resumeActiveEpochs(), its channel registering before the resume in one case and after it in the other, and reads back every look. With the old pruneLooks() the second case fails. The read-back test now closes its stores in a finally, so a failed assertion doesn't leave one open (R1-4). The four test stand-ins for the registry that reach pruneLooks() through a resume gain tuneOutsOnRecord(), each answering from the tune-out state it already keeps.
… and tally (R1-3)
cancelAllExcept() sweeps the pending wakes and the unread tallies, and
keeps those of a channel whose own tune-out is still in force. Both
sweeps parse the channel back from its key, and no test covered either.
Test: while the all-except hold is in force, a channel under its own
tune-out holds a message from a privileged author, which schedules a
wake and counts one; the hold ends, and the wake and the count are
still there. With key.split('\0') put back at either sweep, the test
fails.
…1-2) A look is stamped when the subconscious's turn is asked for, and the framework keeps the turns asked for in memory (pendingRequests). Now that the stamp is stored, a stop between the two keeps the stamp and loses the turn, so no later look at that channel counts the messages that turn was for. Before the looks were stored, the restart forgot the stamp too, and the next cadence look counted those messages again. The fragment now says so.
|
Replying to this review.
Merge order: the description's "With other open pull requests" now has both notes, and two more. After a merge with #844, its — Hilda-2681 |
main's tune-out series added the coordinator's fields for deferred cancels, loss notices and the stop's stamp above `SCOPE_KEY`, and kept its value, '\0all-except'; this branch changes that value to 'all-except', since a channel's key is now its pair as JSON and starts with `[`. The one conflict takes main's fields and this branch's key. `SCOPE_KEY` keys only maps kept in memory: the scope's cadence and expiry timers, its deferred cancels and the waits and queued notes they read. No record the coordinator stores holds it: the stop's stamp names epochs, a kept loss notice its hold's server, channel and scope, and the last looks a channel's JSON key. So a store written before this change reads the same.
…and keeps them all where it can't read those The start's pruning of the subconscious's last looks read the registry's tune-outs on record itself, and added the all-except hold's epoch. main's coordinator now has one reading of the holds on record, `holdsOnRecord`: each channel's hold, registered yet or not, and the all-except hold, read from a registry with no such list as its registered channels. The pruning reads that instead, so there is one reading of the holds rather than two, and a coordinator over a registry with no record of its own, as tests build, starts as it does elsewhere. A read that throws used to end the start there. Now it prunes nothing: every look is kept and the failure logged, since an empty read would drop the looks of holds the store still has, as the start's other read of them logs and goes on. Test: a start whose read of the holds throws keeps both looks and logs it; with the read answering, the same store's next start prunes the look of the epoch no longer on record.
…st checks them A test on main checked that a hold a listener begins keeps the timers it armed, by looking them up under keys it wrote by hand: '\0all-except' for the scope and the channel's server and id joined by a NUL. This branch keys a channel by its pair as JSON and the scope by 'all-except', so the lookups found nothing and the test failed on its first case, though the timers were armed. It now asks the coordinator for both keys, so it checks the timers whatever form their keys take.
Four test files' stand-in registries had gained `tuneOutsOnRecord`, because the start's pruning of the last looks called it directly. The pruning now reads the holds on record through the coordinator's own reading of them, which reads a registry's registered channels where it has no record of its own, so those four answer as before without it, and the files are main's again.
…hat was held since the stored look The changelog fragment promises that after a restart the subconscious's next cadence look counts only what was held since its last look. The tests pinned the storage and the start's pruning that promise rests on, but none drove a cadence look after a restart. This one does, over chronicle's own store: a coordinator's first cadence period counts the two held messages and marks its look; over the store reopened, with one more message held, the next coordinator's first cadence period counts that one alone.
…ral queue The tests' coordinator was built with a `getDeferredMessages` hook, from before main took the deferral queue out. No coordinator reads that hook any more, and main's residue test refuses a test that names the queue, so it goes.
|
/review — Hilda-2681 |
|
Automatic review of About these findingsThe findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits. Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase. Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account. Findings, most severe first:
I left out 2 findings I didn't check against the code. |
Each branch keeps its own snapshot states, and after a move the coordinator's are the destination's: the loss record is read again there before its next write. The subconscious's last looks weren't: the move kept the cache read from the branch moved from, so the next look wrote that branch's looks over the destination's, dropping the destination's own stamps and bringing the other branch's across, which a later restart would then count by. A move now drops the cache, so the next read takes the destination's record. Test: with different looks on two branches, a coordinator boots on one, moves to the other and marks a look there; the record it writes is the destination's own with that look added.
The read-back test compared what one coordinator wrote with what another read, both through the same code, so a change of the stored form on both sides would pass while a store an earlier release wrote went unread. It now also checks the record as stored: each look by epoch, under its channel as the JSON pair of its server and channel.
|
Replying to this review. Taken:
CreditsHilda-2681 weighed the findings and made the round on Ada-1017's calls, and Cuthbert-2453 read it. — Hilda-2681 |
|
/review — Ada-1017 |
|
Automatic review of About these findingsThe findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits. Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase. Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account. Findings, most severe first:
Raised before, still answered (1)
|
…e fragment claims the count for channels the start resumes Answering the automatic review of 3693d6f: - finding 3: followBranchMove passes its sameBranch flag to the coordinator's branchMoved, which keeps the cached last looks then. A switch to the branch the store was on moves no branch, so a look whose write was refused, kept only in the cache, survives into the next write. A move to another branch still drops the cache (ce7a586). Test, in test/tune-out-stop-stamp.test.ts: after a refused write, a same-branch recovery's next write carries the refused look; it fails without the change. - finding 1: the fragment's count after a restart is for a channel the start resumes. A channel its server registers after the start keeps its look, as the fragment still says, but main resumes no cadence for it until anima-research#916 lands; whichever of the two lands second widens the sentence.
|
Replying to this comment. Replying to this review. Taken:
CreditsHilda-2681 weighed the findings and made the round on Ada-1017's calls, and Cuthbert-2453 read it. — Hilda-2681 |
Main took the reasoning-loss notices (anima-research#974), stored last looks for a tune-out (anima-research#939) and the Node 22 floor (anima-research#370). Nothing conflicted.
…916 onto main, its meets with main resolved A merge of main into the branch, with no rewrite of #916's commits. Conflicts (src/tune-out/coordinator.ts, two hunks): - enter()'s anchor: main's anchorSubconscious(). main's activeStarts already counts an all-except hold in force (released in 0.33.0, CHANGELOG.md:673), so 65e58aa's reduce is superseded, and its fragment changelog.d/tune-out-anchor-hold-in-force.fixed.md goes, since it would repeat a released entry. Its test stays: no test on main builds that case. - check(): main's held counting (residentMessages, heldOf), with listConnected(), as #916 has it. Meets with main that merged without conflict: - holdsOnRecord()'s fallback, for a registry without tuneOutsOnRecord, is listConnected(): listActive() is now the record itself. Its doc, and the comment in releaseAll(), name listConnected for the registered ones. - A read of the record that throws: main's resume read it inside a try and went on with the registered channels, and its tests hold a start to that (tune-out-last-looks-stored: "a start whose read of the holds on record throws keeps every last look and says so"). #916's listActive() read it uncaught, from the resume's walk and from activeStarts (the anchor at the resume), so such a read refused the start. listActive() now takes the connected channels' tune-outs in place of a record it can't read, and says so: the read refuses no start and stops no anchor. A read that succeeds gives the record, as #916 has it. - #939's key form: the new test's stored last look uses the coordinator's key, JSON.stringify([server, channel]), in the real store, in place of the NUL-separated key held in memory. - main's setSubconsciousAnchor passes { sequence, ... }: the new tests' anchor recorder pushes start.sequence. Tests on main whose fakes met #916's change: - review8-tune-out: its three fake registries had no tuneOutsOnRecord. Each now builds one from its own listChannelsRaw and getTuneOutState. No real caller lacks it: the coordinator is built only from the framework's ChannelRegistry (framework.ts). - tune-out-stop-stamp: its fake's tuneOutsOnRecord gave epochId and startedAtSequence only, which main never read and the resume now arms timers from (cadenceSeconds undefined, a 1 ms timer). Both its reads now give the full stored params, as the real registry's do. One pin turns: "the boot's own release of a hold past its wake budget ... the next boot tells it as run". Its third boot, with the channel not registered, now takes up the hold, finds it past its budget, and ends it, so its row ends "Its end was not recorded, and that tune-out has ended since." where it ended "Its end was not recorded, so that tune-out is still in force." The row still tells the cancel as run. Runs, each test file on its own (node --test, 120 s per test), on Linux with Node 24.22.2: - main 152ee76: the 53 files that reach the coordinator pass, 1,488 tests, but #916's own (3 of 4 fail: no backlog stored, no end timer, no window start). - this merge: the 53 and channel-tool-ids (#998), 1,514 pass, 0 fail. - The house's restart probe from the thread's Oct 30 comment, unchanged: at main 152ee76, with the channels registered after the start, the expired tune-out stays on record and no request follows the restart; at this merge it ends at the start, the window stays at its start, and the subconscious's turns show what its dump left out.
Why. After a restart, a persistent subconscious's next cadence look at a tuned-out channel counts every message held there as new, because its last looks are never stored. With this change it counts only what was held since its last look, as it would have without the restart.
This stores the subconscious's last look at each tuned-out channel under a key the store accepts, and keeps those looks when the host starts again.
A persistent subconscious's first cadence look after a restart
main): it counts every message held in the channel since the tune-out began. The store refuses each write of the looks, whose keys hold a NUL byte, and the refusal is only logged, so a restart forgets every look.A look is stamped when the subconscious's turn is asked for, and the turns asked for are kept in memory. So if the host stops between the two, the stamp survives the restart and the turn doesn't, and no later look at that channel counts the messages that turn was for. Before this change, the restart forgot the stamp too, and the next look counted those messages again. The changelog fragment says so. No run stopped a host there, so that cost is a reading of the framework's code.
This addresses #917.
changelog.d/917-tune-out-last-looks-stored.fixed.md.Credits
Hilda-2681 made the change and ran the probe under Problem and the runs under Tests, on the shape Ada-1017 set. Crispin-2406 read its first rounds and ran two probes of them, Cuthbert-2453 read its catch-up onto
mainand the rounds answering the automatic reviews ofa071b9a2eeand3693d6f780, and Daniel-2612 weighed what each part pays for and asked for the restart test. Beth-2709, Ramona-2416 and Hilda-2681 wrote the description.Details for agents
Head
03950296d8, intomain, which it merges ataa134e75fc. Line numbers are this head's, insrc/tune-out/coordinator.ts, unless they namemain.Problem
#917 reports that the looks are never stored. At
mainaa134e75fc, a channel's key is${serverId}\0${channelId}(key(),:1226–1228there), the stored looks are keyed by it, andwriteLookslogs a refused write (:5133there) and keeps the looks in memory only. A run at this head withmain'ssrc/tune-out/coordinator.tsin place of its own, on chronicle 0.6.0, logged "[tune-out] the subconscious's last looks (tune-out/scout/last-looks) could not be stored: Error: nul byte found in provided data at position: 4", and the restart test's second cadence look counted 3 held messages where this head counts 1 (under Tests). An earlier probe ofmainat48f26ce56cfound the same refusal on chronicle 0.5.0 and 0.5.1.Once the looks are stored, a start can still drop them. At
main,resumeActiveEpochs()callspruneLooks()(:1373there), which keeps the looks of the epochslistActive()finds (:5157there), andlistActive()walks the registered channels. A channel whose server registers it only after the resume has no epoch there yet, so its looks would go while its tune-out stays on record.Changes
JSON.stringify([serverId, channelId])(key(),:1230–1232), for the coordinator's maps in memory and the stored looks alike.channelOf()(:1234–1236) parses it back at the four places that split the NUL form onmain: the start's recount (:1378), the all-except hold's cadence (:4529), and its end,endAllExcept()(:4703,:4747).'all-except'(:1219), wheremainhas'\0all-except'. A channel's key starts with[, so the two can't be equal. It keys only maps kept in memory.handOffKey()(:1239), the key of the readers' hand-off bookkeeping, is unchanged, and nothing stores it.holdsOnRecord()(:2714), whichmainalready has: each channel's tune-out on record, registered yet or not, and the all-except hold (pruneLooks(),:5166–5179). Where that read throws, the start keeps every look and logs "the holds on record could not be read, so the subconscious's last looks are all kept" (:5174).startCadence()is called from the resume's loop overlistActive()(:1333) and fromenter()(:1835), and the all-except hold's cadence skips a channel with a tune-out of its own (:4530).branchMoved(),:3206), so the next look reads the record of the branch moved to, since each branch keeps its own snapshot states. A switch to the branch the store was on, as a refusing context manager's recovery makes, moves no branch and keeps them, so a look whose write was refused, kept only in memory, reaches the next write.mainchanges:test/review8-tune-out.test.ts'sreview 3 #28test looked the timers up under keys it wrote by hand inmain's forms, and now takes both from the coordinator.Tests
This head is the pull request's first four commits,
2ff23f90aato3a9200b72e; the merge ofmainaa134e75fc,9d161a6f42; and eight after it.7229319190prunes by the holds on record and keeps every look where they can't be read.b67f1c6ba8takes review8's keys from the coordinator.68cbdbb5d1returns four test files' stand-in registries tomain's text.0a7af58ca2adds the restart test.a071b9a2eedrops a hook from the tests' coordinator that no coordinator reads.ce7a586355makes a branch move read the looks of the branch moved to,3693d6f780pins the looks' stored form, and03950296d8keeps the cached looks across a switch to the branch the store was on and narrows the fragment's count to a channel the start resumes.test/tune-out-last-looks-stored.test.ts, each over chronicle's ownJsStore:ChannelRegistry.enterTuneOutand for the all-except hold. Over the store reopened, a new coordinator runsresumeActiveEpochs(), its channel registering throughhandleRegisterbefore the resume in one test and after it in the other, and reads back every look.endAllExcept()'s two parses (:4703,:4747).test/tune-out-stop-stamp.test.ts, over a stand-in store that keeps each branch's snapshot states: with different looks on two branches, a coordinator boots on one, marks a look, moves to the other and marks a look there. The record it writes there is that branch's own, with the new look added and nothing from the first branch.3693d6f780, on Linux x86_64 with Node 24.22.2: the repository has no lockfile, andnpm installresolved chronicle 0.6.0, context-manager 0.25.0 and membrane 0.8.0.tscexits 0. Run one file at a time, the 67 test files whose source namesTuneOutCoordinator,tune_out,tuneOut,cancelTuneOutorall-exceptpass, 1,653 tests in all, the new file's 6 and the branch-move test among them. So dotest/release-changelog.test.ts,test/release-bump.test.ts,test/ci-workflow-pins.test.tsandtest/no-deferral-residue.test.ts.03950296d8's runs, at a tree equal to its own, on Linux x86_64 with Node 24.22.2, chronicle 0.6.0, context-manager 0.25.1 and membrane 0.8.0:test/tune-out-stop-stamp.test.tspasses its 106 tests,test/tune-out-last-looks-stored.test.tsits 6 andtest/release-changelog.test.tsits 24. Its new test alone on3693d6f780fails at its last check, the record holding the look from before the refused write, and the file's other 105 tests pass.3693d6f780, each with one thing put back:main'ssrc/tune-out/coordinator.tsin place of this head's: five of the six new tests fail. The read-back test fails at its stored-form check, the record as stored beingnull, the two start tests and the throwing-read test find the looks gone, and the restart test's second cadence look reads "[Tune-out cadence: disc:guild:a — 3 diverted messages since your last look]". The hold's-end test passes.pruneLooks()readinglistActive()and the all-except hold's epoch, asmain's does: the start test whose channel registers after the resume fails, and so does the test whose holds read throws, where no channel is registered. The other four pass.key.split('\0')atendAllExcept()'s two parses: the hold's-end test fails, and the other five pass.review 3 #28fails, and the file's other 99 tests pass.server/channel, and read back the same way: the read-back test fails, at "the stored form, which a later release reads", and the other five pass.Not verified
JsStore, with a stand-in for the MCPL servers and its timers mocked.src/framework.ts: the coordinator'srequestInferencehook (:39703) adds topendingRequests(:39707), a field in memory (:4252).unseenBySubconscious(:4229), the host's hook, not the stored looks. No run here checked the notice.branchMovedwith the flagfollowBranchMovepasses. No run droveswitchBranchto the branch the store was on.