Skip to content

fix: every agent on the store works again after a rollback, suppression, branch switch, undo or redo - #353

Merged
nissa-seru merged 55 commits into
anima-research:mainfrom
nissa-seru:fix/reinit-after-branch-move
Oct 26, 2026
Merged

nissa-seru merged 55 commits into
anima-research:mainfrom
nissa-seru:fix/reinit-after-branch-move

Conversation

@nissa-seru

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

Copy link
Copy Markdown
Contributor

After a rollback, a suppression, a branch switch, an undo or a redo, every registered agent sharing the store compiles again without a restart, unless its re-initialization fails, which raises an alert and is recovered by a switchBranch(). !Fixes #326.

What was wrong

A context manager loads its strategy for one branch generation of the store. context-manager re-initializes a manager when that manager makes a move itself, with switchBranch() or fork(), and re-initializes no other. So:

  • after rollbackToMessage() or suppressMessages(), every agent but the one named kept a strategy loaded for the branch the store had left;
  • after switchBranch(), undoLastTurn() or redo(), which switch the store directly, every agent did, the one named included.

An AutobiographicalStrategy then refuses that agent's compiles, its readiness and its maintenance ("requires reinitialization for the current branch generation") until a restart opens every manager again. #326 has the probes, on context-manager 0.14.0 and 0.13.0.

The change

Each move re-initializes the managers it didn't move before it releases the store. It sets each manager's own strategy again (cm.setStrategy(cm.getStrategy())), which initializes the strategy for the store's current generation without advancing it. A switchBranch() to the same branch would advance the generation, and leave stale every manager re-initialized before it. The managers re-initialize one at a time, with a turn of the event loop before each, so the host goes on handling events between them. A manager's own re-initialization can hold the event loop throughout, as an autobiographical strategy's does: in the probe under Evidence, the event loop turned once per manager, the turn before it.

  • rollbackToMessage() and suppressMessages(): every agent but the one named, once the store has left the source branch, and each waits for them before it returns and lets the store go. When a rollback's switch fails and it switches back to the source, the store has moved twice, so the other agents are re-initialized then too. One refused before the store moves, such as one whose branch name is already taken, re-initializes no other manager and raises no alert.
  • switchBranch(): every agent, alongside the config mount, before the store is released. A switch to the branch the store is already on re-initializes every manager, which is how to recover one whose re-initialization failed.
  • undoLastTurn() and redo() stay synchronous, since the API server, the host/command undo and hosts read their results at once. They now reserve the store as a surgery does, as fix(framework): stop() waits for an operator's operation already running, and refuses new ones #325 made switchBranch() do. Each returns once the store has moved, and the reservation is held until a re-initialization of every manager has finished, after the call returns. Each result carries settled when the store moved: a promise that settles once that reservation is released and never rejects, so a caller can wait for the managers before it compiles on one or writes. The API server's undo and redo await it before they broadcast branch:switched and answer (Anarchid's R3-1). It waits first for maintenance already running, a pass or an over-budget drain kick, so no setStrategy overlaps a tick: its first pass starts inside the call when none is running, and after it returns otherwise. As with a surgery (fix: what still assumed context-manager 0.13, and removing a message stored as a body #352), a maintenance provider call made under the reservation, or one waiting at the provider gate when it's taken, is withdrawn without being sent, so that wait is never a wait on a cooldown. The move itself is synchronous, so a tick running at that moment still runs across it, as on main. An undo or redo made meanwhile moves the store again under the same reservation, and the re-initialization runs again for the branch the store ends on. Once its last pass is done, nothing joins it: one made from a listener of its report meets the hold and is refused as busy. So back-to-back calls keep working, such as a multi-turn host/command undo, or an undo and then a redo from the web API.
  • The host/command turn undo waits for that re-initialization before its lastVisible preview, since the preview compiles. On main the preview failed, and the command answered lastVisible: null.
  • A failed re-initialization leaves that manager refusing its work, as context-manager leaves a failed initialization, and the move stands. It's said on the console, in failures.log and in an ops alert, context-reinit-failed, which names the agent and the move. Each failure is logged and traced, and the alert webhook posts at most once per agent and kind in 15 minutes, as opsAlert does for any alert without a key of its own; a disposal's alert has a key of its own, so a refusal posted for the same agent doesn't silence it. Each alert opens with what happened and what to do or what happens next, in at most 310 characters (store-held-long's in at most 333, with counts of up to nine digits), within the 343 to 349 the webhook keeps of the start of a message of 501 to 9,999,999 characters, by its length. Then come the branches it names, and its cause last, since a cause can be an error's whole diagnostic. The remedy is the same for every agent: a switchBranch() to the current branch re-initializes the manager and stores the writes kept queued for its agent, at resume on a quiesced host, and a restart can lose those writes (R3-5). It says nothing of what a restart does to the agent itself, which differs by agent: a restart creates the configured agents again, and stop() ends each conversation agent and each run. An agent that has left by the time its re-initialization fails, as a run its watchdog ended, isn't reported, since it refuses no one's work. stop() says how many writes its last flush leaves queued for an agent whose store didn't take them, and whether the queue keeps them for boot (R3-5). That line, and main's line for writes left queued behind a stream still running, say the persisted queue's record is behind, rather than that boot keeps them, when the record's last write failed or met the cap.
  • A hold that runs long is said. No turn starts while a move holds the store, and nothing bounds what the hold waits on: maintenance already running, the move itself, and each manager's re-initialization. A move whose hold is still up after 60 s raises one ops alert, store-held-long. It opens with what the hold does and what a restart does: it ends the move, the conversation agents and the runs, and re-initializes the configured agents, and writes still queued at exit survive only in a persisted deferral queue. Then it names the agent the move was for, and the manager its pass is re-initializing or the maintenance it's waiting for, after the opening, since names have no bound. It's raised for every move that holds the store: an undo or redo, and main's rollback, suppression and branch switch (R3-4).
  • A failed restore says where the store is. When a rollback or suppression fails after the store moved and its switch back to the source fails too, it raises the same alert for the agent it named. A switch selects its branch before it initializes, so when only the restore's initialization fails, the store is back on the source; when the restore can't select the source, the store stays on the branch the failed operation left it on. The error and the alert say which, and say that the agent refuses its work until a switchBranch(), to the current branch or to the branch it should run on, re-initializes it, with the same remedy as any refusing manager's. A suppression whose removals failed after its fork is the exception: its fork initialized the agent's manager on the new branch, so when its restore can't select the source, the error and the alert say the agent serves from that branch, which may be partly suppressed, until a switchBranch() moves it. When its restore selects the source and then fails to initialize there, the agent refuses its work, as after any other failed restore.
  • A conversation agent the move left without history is disposed (Anarchid's R1-1, his first option). After the managers re-initialize, a conversation agent can have no stored message of its own on the branch the store moved to, as one spawned after the point the move returns to has none there. Re-initialized, it would answer with no history, since a conversation agent is seeded only when it's created. It's now disposed and its channel unbound, and its context-reinit-failed alert says so. Its channel's next qualifying message spawns a fresh fork from the template, under a new name, which needs the template's own context manager to be working. Its messages stay on the branches that hold them, but no agent reads them again, so an undo and then a redo don't bring it back.
    • When it's judged: after a rollback, a branch switch, an undo or a redo, once, after every manager is re-initialized for the branch the store lands on, with no await between the judging and the disposal, so no move lands during it. An undo or redo made during the pass moves the store before the judging, so a fork an undo empties and a redo made meanwhile restores is kept. A suppression judges none: it forks the store at its head, keeps every other agent's messages, and refuses to remove its own agent's last one, so it can't empty one.
    • Empty means no stored message on that branch, which context-manager reads from the store's branch directly (getMessageCount()), whatever its strategy holds. One whose history is there but compiles to nothing, such as messages that are hidden or past its window, stays. One whose own re-initialization failed is disposed too, with the disposal's alert alone, which gives that failure's cause: the refused-work alert's remedy, a switch to the current branch, would dispose it rather than recover it. The agent a rollback or suppression names is never empty after its move: a rollback keeps the message it returns to, and a suppression refuses to remove an agent's last message.
    • The writes held for it: the alert gives how many writes were held for it while the store was held (R3-3), and nothing of where they go, which is its window's release's doing, as on main for any conversation agent disposed: its window takes what its store takes of them, where no agent reads them, and the primary is passed each failed reply, channel notice whose channel decision still stands, or module write for it (Not covered).
    • A disposal that fails partway, as when releasing its window throws, is named in the same alert, and the pass goes on to the next one. The agent is unregistered and its channel unbound before that step.
    • What reaches a conversation agent during the move waits for the move. Its channel message, a correction for it and its idle closure prompt were written straight into its manager, past the deferral the store's reservation gives every agent's writes. While its manager re-initialized, the write was refused and lost; during a rollback's wait for running maintenance, and by reading a suppression's, it landed on the branch the move then left; and during an undo's or redo's wait, it landed in the namespace the move had emptied, so the agent would be kept with that one message. All three are now deferred while a move holds the store, and land after the release. When the store is let go, each write deferred during the move that the store's release stores is stored with its id, as the other flushes of deferred writes store theirs, and traced as message:added with that id, as the turn-start flush traces each message it stores. Before, the store's release stored them without the id, and traced nothing. A disposed agent's held writes go through its window's release instead, untraced, as on main. A coalesced push no longer removes the stored, unread message it updates or withdraws while the store is held. The coalescer handles that as it does any removal that fails: it appends the update, or a retraction's notice, which lands after the release beside the unread message, and a retraction with no notice leaves the message in place. So an unread message withdrawn while the store is held still reaches the agent, where otherwise a retraction leaves no trace of it.
  • No move lands under a conversation agent being created. Creating one waits while any move or surgery holds the store, and a spawn waits before it stores its channel's name counter, so the counter is on the branch the store lands on, where a restart reads it; since its context is seeded from the template's and its manager opens for the branch the store is on. From then until it's registered, it counts as busy: a move made meanwhile is refused as busy. A spawn that comes to create one once a stop() of its lifecycle has begun, under way, done, or failed with no start() since, as one that waited out a hold can, fails as a failed spawn does, rather than open its manager on a store stop() is tearing down or has torn down. A start() begins a new lifecycle, whose spawns go through.
  • A failed undo or redo keeps its turn, and releases the store. By reading main, a failed undo lost its checkpoint there and a failed redo its step. A redo whose branch was deleted drops that step and says so with OperatorActionError (failed), naming the branch and how many redo steps remain, with the switch's own error as its cause. Any other failed redo keeps its step and answers the switch's error, as does one whose branch listing fails as well.
  • A runtime-settings change, reset or cancelled transition is refused while the store is held, before anything is applied. Before, a change set its override and was then refused by the manager re-initializing.
  • A maintenance tick that an undo or redo moved the store under counts as withdrawn when what it meets is context-manager's refusal of work for a branch its manager isn't loaded for, in its catch and in the readiness read after it, and an over-budget drain kick's log says the same. That refusal is the move's: the tick's work was for the branch the store left, which the re-initialization discards. context-manager 0.22.1 gives it as a plain Error with no class or code, whose message carries "requires reinitialization for the current branch generation" or "refused stale branch-scoped mutation", so the check reads those two texts. Any other failure the tick meets keeps its context-maintenance-failed alert. The store's hold starts no new tick, so only a tick running when the move landed is treated this way.

What callers will notice

  • The context managers of the agents a surgery doesn't name keep working after it, and every registered agent's keeps working after a branch switch, undo or redo. There is nothing to change for this.
  • Breaking: an undo or redo while another agent sharing the store is busy or a conversation agent is being created; a switchBranch() while another move holds the store for its re-initialization, two made together included; and a runtime-settings change, reset or cancelled transition while a move or a surgery holds the store are refused with OperatorActionError (agent-busy). By our reading of 0.27.0's code each went ahead there, except a second switch while the first still waited for running maintenance or a workspace module's mount; a run shows two switches made together, with neither, both going ahead on 0.27.0. The changelog's breaking fragment says so.
  • undoLastTurn() and redo() are refused with OperatorActionError (agent-busy) while any agent sharing the store is busy, or a conversation agent is being created, the one named included, which used to throw a plain Error. Before, they switched the store under another agent's turn in flight. A refused undo or redo keeps its checkpoint or redo entry. A failed one does too, unless it's a redo whose branch no longer exists, which drops that step and now says so with OperatorActionError (failed). By reading main, a failed undo or redo lost its checkpoint or step there, a redo whose branch was gone included.
  • Just after undoLastTurn() or redo() returns, the store stays reserved while the managers re-initialize. No turn starts, and one asked for meanwhile starts after. Messages for the agents defer as they do during a surgery, a conversation agent's included, and rollbackToMessage(), suppressMessages() and switchBranch() are refused as busy. Another undo or redo isn't refused, as long as every agent is idle. A runtime-settings change, reset or cancelled transition made then, or during a surgery, is refused with OperatorActionError (agent-busy) before any of it is applied. A context-settings preview made then is refused by context-manager until that agent's manager is current, as during a surgery's switch.
  • While the store is held, a coalesced update or retraction no longer removes the stored, unread message it updates or withdraws. The update, or the retraction's notice, lands after the store is released, and a retraction with no notice leaves the message in place, so an agent can read a message withdrawn while the store was held. Tests show each during a branch switch's hold; by reading, it holds for a surgery, an undo and a redo too, since every hold declines the removal alike.
  • Two switchBranch() calls made together: the second is refused as busy while the first holds the store, with or without a workspace module. Before, without one, the first released the store before any await and both went through. The API server's test for two switches handled together now expects that.
  • A new ops alert kind, context-reinit-failed. Its remedy says what a restart costs, and stop() logs a line for each agent whose writes it leaves queued because its store didn't take them. Each of its alerts gives its cause last.
  • stop()'s line for writes left queued behind a stream still running says the persisted queue's record is behind, rather than that boot keeps them, when the record's last write failed or met the cap.
  • A spawn that comes to create a conversation agent once a stop() of its lifecycle has begun, under way, done, or failed with no start() since, fails, and is reported as any failed spawn is; the channel message that started it is dropped with that spawn.
  • A new ops alert kind, store-held-long, for a move whose hold on the store is still up after 60 s. It's raised for every move that holds the store, main's rollback, suppression and branch switch included, not only an undo or redo.
  • undoLastTurn()'s and redo()'s results carry settled when the store moved. It settles once the store is released, every agent's context manager re-initialized or its failure alerted, and never rejects. Await it before compiling on an agent's own context manager, which refuses until it's re-initialized, or before a write meant to land at once, which the reservation defers.
  • The API server's undo and redo answer, and broadcast branch:switched, once the store is released. In the probe under Evidence, an undo held the store for 4.5 s with 16 agents of 1,000 stored messages each, and for 10.9 s with 32 of 500. Their answers are as before, without the promise.
  • When a move disposes a conversation agent, its alert says how many writes were held for it while the store was held and, when its own re-initialization had failed, that failure's cause.
  • A conversation agent with no stored message of its own on the branch the store moves to, such as one spawned after the point a move returns to, is disposed and its channel unbound, with an alert saying so and that a redo won't bring it back. Its channel's next qualifying message spawns a fresh fork, which needs the template's own context manager to be working. Messages that reach a conversation agent while a move holds the store land after the release. Creating a conversation agent waits while the store is held, and a move made while one is being created is refused as busy.
  • A maintenance tick that an undo or redo moves the store under no longer raises context-maintenance-failed for context-manager's refusal of its stale work. Any other failure it meets still does.

What each part is for

The change is for one thing: after any move of the store's branch, the context manager of every agent sharing the store works again without a restart, and one that can't is named in an alert that says how to recover it. Each part below serves that, or keeps something that runs during a move from undoing it.

  • Re-initializing every other manager after a move (reinitializeManagers) is the fix itself. context-manager refuses a strategy loaded for a branch generation the store has left, and this framework made the move, so it re-initializes them. Setting each manager's own strategy again is how a manager is re-initialized without advancing the generation, as a second switchBranch() would. Weighed: context-manager re-initializing a stale strategy itself at its next use. That changes context-manager's contract, runs each re-initialization in that agent's next turn rather than under the move's hold, and still leaves the conversation agents below to this framework.
  • One manager at a time, with a turn of the event loop before each (Anarchid's R2-2), costs one setImmediate per agent. Side by side, a pass held the event loop for all of it: 5.1 s with 16 agents of 1,000 messages each, in the probe under Evidence.
  • undoLastTurn() and redo() stay synchronous (beginSyncMove, reinitializeAfterSyncMove, syncMoveReinit). The API server, the host/command undo and hosts read their results at once, so making them async would break those callers. Instead they hold the store until the managers are current, after they return. An undo or redo made meanwhile joins that hold, which costs the again flag and the loop that runs the pass again. Without the join, the second turn of a multi-turn host/command undo, or a redo right after an undo, would be refused as busy.
  • settled on undo's and redo's results (Anarchid's R3-1) is how a caller waits for the reservation those calls leave held: before it, only the host/command undo could, through a private field. It costs a field on two results, carrying the promise the re-initialization already held (SyncMoveReinit.done). The API server's wait makes its undo and redo answer seconds later with many agents, which is when a client's next read would otherwise be refused.
  • context-reinit-failed, with its remedy (reportReinitFailures, refusedWorkAlert): a manager whose re-initialization fails refuses all its work until something re-initializes it, so whoever runs the host needs to hear which agent it is and how to recover it, through the alert webhook as well as the console. A rollback's or suppression's failed restore leaves the agent it named the same way, so it raises the same alert (restoreAlert), unless its restore couldn't select the source after a suppression's removals failed: that agent's manager is current on the suppression's branch, and the alert says it serves from there. The remedy says what a restart costs (R3-5): the writes kept queued for that agent, which a switchBranch() recovery stores. stop()'s line, one per agent, says the same when the cost is paid.
  • store-held-long (R3-4; reportLongHold, one timer per hold, armed in reserveStoreForSurgery and cleared at its release): nothing bounds what a hold waits on, and while it's up every agent waits, so a hold that runs long needs saying, whichever step is holding it. A deadline that releases the store isn't safe without a cancellation contract in context-manager, so it's an alert. It costs one timer per hold, a field on the hold naming the manager a pass is on, a field holding the threshold, which a test shortens, and three nouns for the hold's verbs (MOVE_NOUNS), so the alert names the move as the move's other alerts do ("the rollback", where a refusal says "Cannot roll back"). A maintenance call already sent counts toward the threshold, so a slow provider call can raise it on a move that's working; the store is held then too, and the alert says that's what the move is waiting for.
  • Disposing a conversation agent the move emptied (disposeEmptiedForks, Anarchid's R1-1, his first option): without it, a conversation agent spawned after the point the move returns to answers with no history. Weighed: seeding it again from the template, as an earlier round did, which kept its name, so a redo found its history. That serves per-channel conversation routing, which 0.27.0 marks deprecated, and cost a template-health path, a check for a store moved mid-seed, a callback threaded through each pass, and the seed's extraction from creation. Disposal costs one synchronous pass after the last re-initialization, and an undo then a redo don't bring a disposed agent's conversation back. Its alert counts the writes held for it (R3-3).
  • One judgment, after the last pass: the disposal judges the branch the store lands on once, rather than once per pass. It runs in the same step as the loop's check that no undo or redo joined the last pass, with nothing awaited between, so no move lands between them; placed at the end of reinitializeManagers instead, it would leave a microtask before that check, in which a redo could join after a fork was disposed. It costs nothing over calling it inside the loop.
  • One alert for a disposed fork whose re-initialization failed, its failure dropped from the list: its message count reads the store's branch directly, and the refused-work remedy would dispose it rather than recover it. It costs a filter.
  • A disposal that fails partway is reported, and the pass goes on (cleanupFailed, one catch per conversation agent, for Greptile's review of 7bfa5622). Without it, one fork's disposal that threw escaped the move's pass, so the forks after it weren't judged and no failure was reported. It costs a field on the failure and a catch.
  • A disposed fork's failed re-initialization in its disposal alert (reinitFailed): its refused-work alert is dropped for the disposal's, so without it that cause would be reported nowhere. It costs a field and a sentence, which comes before a failed cleanup's.
  • The disposal alert's own webhook key ('disposed'): the webhook posts at most once per agent and kind in 15 minutes, so a refusal posted for a fork within that time would silence its disposal, and the last post an operator saw would say a switch recovers a fork that's gone. A fork's name isn't reused, so the key costs at most one more post per fork.
  • No disposal judgment after a suppression: a suppression forks the store at its head, keeps every other agent's messages, and refuses to remove its own agent's last one, so it can't empty a conversation agent. A judgment there would be a pass that can dispose nothing.
  • The log line for a failure in an undo's or redo's pass names the re-initialization or the release: its catch covers both, and naming the release alone sent a reader to the wrong step. It costs no code.
  • moved, in the rollback's and the suppression's catch blocks: a move refused before it switched the store left every manager where it was, so none is re-initialized and no alert is raised; one that switched and then failed has moved the store, and back again unless its restore failed too, so the others are re-initialized after its restore. It costs a branch read in each catch.
  • A spawn's check for a stop() of its lifecycle, after its wait for a hold (stopPromise, or running cleared with the queue closed): stop() doesn't wait for a spawn, so one that waited out a hold into a stop() would open a manager on a store being torn down, or one after a failed stop() on a framework that no longer runs. stopPromise is set from stop()'s first steps, which can emit traces while the framework still runs, and is cleared by a failed stop() and by an addModule() after a completed one; running cleared with the queue closed covers those, until a start(). It costs one condition. stopHasBegun(), the operator calls' rule, reads the closed queue whether or not a start() came after, so it would refuse every spawn after a stop() and a start().
  • stop()'s two queued-write lines say when the persisted queue's record is behind (keptWhere): a mirrored queue whose last write failed or met the cap keeps the flag that says it's mirrored, and boot reads only what the record last held, so "the persisted queue keeps them for boot" would be false then. It costs one helper both lines call.
  • The re-initialization alerts' texts, built by shape (refusedWorkAlert, restoreAlert, disposalAlert, and longHoldAlert for store-held-long): each gives its opening, the outcome and what to do or what happens next, short enough to stay in the start the webhook keeps of a long message, and its message: the opening, then the branches it names and the count of held writes, then the cause, which can be an error's whole diagnostic. A test builds every shape, with long causes and branch names, and checks each opening against opsWebhookText, the cut opsAlert posts. REINIT_MOVES types the moves, so a new one joins the test's shapes. Without them, a wording change could push an alert's remedy out of the webhook's post unseen, as happened twice in this round, each time found only by a review. It costs four small functions and a typed list, and shorter remedies. store-held-long names two agents before it was built this way, and a conversation agent's name runs to about 100 characters, so the webhook's cut could drop its restart warning.
  • What a failed restore says about a suppression's current manager (restoreSourceBranch's managerCurrent): without it, the alert said such an agent refuses its work while it served from a branch that may be partly suppressed. It costs a parameter the removals' catch passes. restoreAlert takes it for a suppression only, by its type, so no caller can build that text for a rollback, whose fork never initializes the manager it names.
  • restoreSourceBranch's move: the restore's alert names the move that failed, "The rollback failed" or "The suppression failed", as the move's other alerts name it. Without it, one of the two would be worded as the other. It costs a parameter its three callers pass, and a test pins the suppression's at its fork's call site.
  • RECOVERY: the one remedy every refusing alert gives, so refusedWorkAlert's and restoreAlert's can't drift apart. It costs a constant.
  • A test's probe of what it reached (initialized): the test in which an agent leaves during its own re-initialization records that the re-initialization ran, so it can't pass without reaching the point its title names. It costs a flag.
  • The tests' helpers (says, courierModule, realTurn, heldTurn, holdFor, the forks' harness with its membrane, courier and held tick, gateReinit, nextForkFor, withRunningTick): each puts a test's setup through a route a caller has, a module's write, a real turn, a held stream, a real maintenance tick or a channel message, or checks an alert's facts rather than its sentence. Without them, each test would repeat that setup, or reach the same state through the framework's internals.
  • bounded, in the coalescing tests' held-store case: its waits for the switch's re-initialization and for the push's reply fail after 10 s rather than hang, as the re-initialization file's until() does.
  • A conversation agent's writes wait for the move (writeToFork, Anarchid's R2-1). They now take the deferral every other agent's writes take under a hold, rather than a path of their own. The release stores each deferred write with its id through fix(deferral): hold window-only messages to the turn's end; injected messages are read #286's flush helper, which it now uses, as the other flushes store theirs. The release traces each write it stores, through an option on that helper (traceAs), as the turn-start flush does, since a write's trace at its deferral has no id. A coalesced removal leaves a stored message alone while the store is held (removePlacement, one line): it would touch a manager the hold is protecting. A module's edit or removal of a stored message, and a write carrying a fresh tool result, still reach a manager during a hold, as on main (R3-2; Not covered). The coalescer handles the refusal as any failed removal, so a retraction that meets a hold doesn't keep the unread message from the agent (above). Weighed: queueing the removal for the release. That keeps the retraction's promise, but the coalescer would be told the message is gone before it is, so a crash or a failed removal at the release would leave a copy it believes removed, and the queue would have to be stored, as the deferred writes are.
  • Creating a conversation agent waits for a hold, and counts as busy until it's registered (released, conversationSpawnsInFlight). Its manager opens for the branch the store is on, and it's seeded from the template's compile, so a move landing during the spawn would seed it from the wrong branch. Refusing the spawn instead would leave the message that triggered it undelivered, as a failed spawn does.
  • A spawn's wait for a hold before it stores its channel's name counter (the same one-line wait as creation's): during a rollback's or suppression's wait for maintenance the store hasn't moved yet, so a counter stored then lands on the branch the store leaves, and after a restart the channel's next fork takes the name of the one made meanwhile, over its namespace. It costs a line, in the spawn as in creation, since a helper awaited there would yield a microtask after its last check, in which a move could take the store.
  • Refusing runtime-settings changes under a hold (refuseRuntimeSettingsChangeUnderHold): every manager now re-initializes under a hold, and one that's re-initializing refuses the settings read that follows a change, so a change would otherwise be applied and then refused, leaving its override behind.
  • Treating context-manager's refusal in a maintenance tick that an undo or redo moved the store under as withdrawn (MOVED_BRANCH_REFUSALS): otherwise each such move raises context-maintenance-failed for a failure the move caused. The refusal has no class or code, so it's told by its two texts, and any other failure keeps its alert. It costs a list of two texts and a function. The hold starts no tick, so only one already running is treated this way.
  • A failed undo or redo keeps its turn. It has to release the store it reserved, and beside that, keeping the turn costs one restore of the checkpoint list for an undo and one push back for a redo. By reading main, a failed undo lost its checkpoint there, which nothing can make again, and a failed redo lost its step. A redo whose branch is gone drops that step and says so: a failed redo now has two outcomes, so its error has to say which happened, or a caller would retry and redo the step beneath.

Cut by a review of the source against what the change needs (77b79fc7): the re-seed, for disposal; the remedy by agent kind (AgentKind and its clauses), for one remedy for every agent; and the disposal alert's account of where the held writes go, for their count. With them went the template's remedies, the check for a store moved mid-seed, the callback threaded through each pass, and the postponed-release field. Creation's emptiness check is main's again, a dry-run compile of the new namespace, since the stored-message count was shared with the re-seed only. The refused-work and restore alerts no longer say that a restart ends a conversation agent or a run.

Cut by an earlier pass (83d9e72f): a redo whose branch was gone also dropped each step beneath it whose branch was gone too, and its message said the step now on top could be redone. Now a redo drops one dead step, which by reading is what main does, and its message says only how many steps remain; the next redo meets the next dead step the same way. The test of two dead steps went with it.

Tests

test/reinit-after-branch-move.test.ts puts two agents with autobiographical strategies on one store, with a module that writes for an agent as any module does. A turn's checkpoint comes from a real turn on the mock provider, and a busy agent from a turn whose stream holds. It checks that:

  • after rollbackToMessage() on one agent, the other compiles and takes a turn, and compiles what the landed branch holds;
  • after suppressMessages() on one agent, the other compiles;
  • after switchBranch(), and after a switch to the branch the store is on, both compile;
  • undoLastTurn() returns with the store moved, and both compile once its settled settles; redo() likewise;
  • an undo and a redo made back to back move the store under one reservation, a rollback meanwhile is refused as busy, and the managers end current with no alert;
  • a turn asked for while the managers re-initialize starts once the store is let go, and stop() waits for an undo's re-initialization;
  • a host/command turn undo answers with its preview, and a host/command undo of two turns undoes both;
  • an undo and a redo are refused while another agent on the store is in a turn, and keep the checkpoint and the redo entry; an undo is refused while its own agent is in a turn;
  • a failed re-initialization raises one context-reinit-failed alert naming its agent, the rollback stands, that agent's compile is refused, and a switch to the current branch recovers it; one that fails after an undo, a branch switch or a suppression names the move;
  • the alert's remedy says a switch to the current branch re-initializes the agent and stores its queued writes, that a restart can lose them, and its cause comes last; stop() says how many writes it leaves queued for an agent whose manager refuses them, and nothing for one whose write the release stored;
  • stop()'s line for such writes, and main's for writes behind a stream still running (in test/stop-with-work-in-flight.test.ts), each for a memory-only queue, a persisted one whose record is current, which says boot keeps them and nothing of a loss, and one whose record is behind, which says why and that a restart can lose them;
  • a write a module makes for an agent during the hold, when that agent's re-initialization fails, isn't stored at the release, and is stored once, in its window, after a switch to the current branch recovers it (R1-2);
  • an agent that leaves during its own re-initialization, which then fails, isn't reported;
  • a redo that overtakes a failing pass reports nothing, and a failing last pass reports once, naming the redo; a redo made from a listener of the last pass's alert is refused as busy, the store stays where the undo left it, and the agent that moved compiles;
  • a rollback whose switch fails, and which switches back to the source, still leaves the other agent current; a suppression whose fork moves the store and then fails restores the source likewise;
  • a rollback whose restore fails names the agent and the branch the store is on, both when the store is back on the source and when the restore can't select it, each with the one remedy, and with a 3,000-character cause the webhook still gets the opening;
  • a suppression whose restore can't select the source raises an alert naming the suppression; one whose removals fail after its fork, and whose restore can't select the source, says its agent serves from the failed branch, and that agent compiles there; one whose restore selects the source and then fails to initialize says its agent refuses its work;
  • a suppression whose fork is refused, and a rollback whose switch is refused, before the store moves, raise no alert, with the other agent's re-initialization set to fail, so a pass that ran would show; and that agent still compiles;
  • an undo whose createBranchAt throws, and a redo whose switchBranch throws, leave the store where it was, released, with the checkpoint or redo entry kept; a redo whose switch and branch listing both fail keeps its step and answers the switch's error;
  • a redo whose branch was deleted drops that step and says so, naming the branch and how many steps remain, with the switch's own error as its cause and the store where it was, and the step beneath it redoes;
  • a runtime-settings change, and a reset or cancelled transition, during an undo's re-initialization, and a change during a rollback's, are refused with nothing applied, and the change and the reset apply once the store is let go;
  • an undo's managers re-initialize after undoLastTurn() returns, one at a time, with a turn of the event loop between each two, and the event loop runs before the last of them has; an undo's re-initialization waits for an over-budget drain kick already running;
  • store-held-long from its timer, at a shortened threshold: an undo and a redo joined, with the second pass held at a gate and a write deferred, name the undo, its agent, the manager being re-initialized and the write; a branch switch held by its config mount after its pass names no manager; and a hold let go before the threshold raises none after it;
  • an undo whose release throws still settles settled, and the release's failure is logged;
  • the API server's undo and redo broadcast branch:switched once the store is let go, so a client that reads the other agent's context on each event gets it, and their answers carry no promise.

With a conversation template and its forks:

  • a fork created while an undo's managers re-initialize waits for them, reads the template after its re-initialization, and holds the landed branch's context with nothing of the undone turn; a move made while a fork is being created is refused as busy, and goes through once it's created, and a rollback asked for in the same step as the mention that spawns a fork is refused as busy too; a spawn that fails clears its name, so a move goes through after it;
  • an undo to before a fork was spawned disposes it, unbinds its channel, and raises one alert saying so, that the next spawn needs the template's own context manager to be working, and that a redo won't bring it back; the channel's next mention, through the framework's own routing, spawns and binds a fresh fork under a new name, and a fork spawned before that point stays bound with its history;
  • a branch switch, a rollback, and a redo back from an undo's branch, each leaving a fork with no stored message of its own, dispose it and unbind its channel;
  • a fork that keeps a clock, emptied by an undo and re-initialized while the pass still holds the store, with a channel message and a correction for it then, each placed for it and deferred, is still disposed: no clock row is written into its emptied namespace meanwhile;
  • a fork that refused its work after a switch to the current branch, and is then disposed by an undo, has both alerts posted to the ops webhook;
  • a fork the move emptied whose own re-initialization fails, with a module's write held for it, is disposed with the disposal's alert alone, counting the write and ending with the re-initialization's cause, and the pass and the release finish;
  • a fork disposed with three writes held for it has an alert counting three; one whose disposal fails partway, when releasing its window throws, is reported with the failure last, and the pass goes on to dispose the next emptied fork;
  • an undo that empties a fork, and a redo made before the pass ends that restores it, leave it bound with its history and raise no alert;
  • a fork with stored history on the landed branch stays, with no alert, even when that history compiles to nothing;
  • while an undo holds the store, a kept fork's channel message and a correction for it are deferred, with nothing written into it meanwhile, and land after the release in order, each traced at its deferral with no id yet and again as it lands, with its stored id; its idle closure prompt is deferred and lands after its history the same way;
  • while a rollback waits for a maintenance tick already running, a fork's channel message is deferred, and lands on the branch the rollback moves to, and a fork spawned meanwhile keeps its name to itself after a restart: the channel's next fork then takes a new name and holds nothing of the first's;
  • a spawn that waits out a hold into a stop() fails, as a failed spawn does; a spawn after a failed stop(), or after a completed stop() and an addModule(), is refused, and one after a start() is made; a spawn started from stop()'s first step, while the framework still runs, is refused.

With a maintenance tick held running:

  • a rollback, and a suppression, still waiting for it when their hold passes a shortened threshold raise store-held-long once, held past the threshold, naming the move and saying it waits for maintenance, and nothing more once the store is let go;
  • an undo's re-initialization waits for it before any manager re-initializes, the ticking agent's included, and a branch switch waits for it before it moves the store;
  • an undo that moves the store under it raises no maintenance-failure alert when the tick meets context-manager's refusal, either of its two texts, or when the readiness read after the tick meets it; one where the tick fails for another reason, or the readiness read does, keeps its alert;
  • an undo, and an undo then a redo, each made while a maintenance call waits at the provider gate: each move withdraws the parked call, which sent nothing, and its re-initialization releases the store, with no maintenance failure reported.

Every alert shape the text builders make, store-held-long's included, with causes of 3,000, 200,000 and 1,000,000 characters, branch and agent names of 200 and more, and store-held-long's counts of nine digits, keeps its opening in what opsWebhookText posts, store-held-long's says what a restart does, and each refusing shape's opening ends with the one remedy; a disposal with neither failure, which carries no cause, is posted whole, and one with both carries both causes.

In test/mcpl-coalescing.test.ts, while a branch switch holds the store, an update of an unread coalesced push, and a retraction with a notice, leave the stored message and land after the release, and a retraction with no notice leaves it and appends nothing; main's test that writes right after an undo awaits its settled first.

The tests wait for an undo's or redo's re-initialization on its settled, and a test waiting for something to happen does it through until(), bounded at 10 s, so it fails rather than hangs when that never comes. They check an alert's facts, the move, the agent, the branches, the remedy and the cause, rather than its whole sentence. Three stand-ins remain where no public route reaches the case with one helper: the parked-call test records its checkpoint directly, since its strategy is never ready for a turn; the busy check on a redo joining a pass fakes another agent's state, since the store's hold lets no turn start then; and the over-budget drain kick already running is a pending promise placed where the framework tracks its kicks, since a real one needs an agent over its budget.

The suppression test removes the first message. On context-manager 0.14.0, after a message in the middle is removed, every compile that follows a dry run is refused. That is a separate context-manager bug, fixed there.

Evidence

At tree 321806f0, this head's code and tests (1269c003; the head, f5dba8f6, adds only changelog fragments), on Linux, on a clean checkout installed fresh by npm install with no lockfile and built from an empty dist, as CI does, on Node 24.22.1, with context-manager 0.22.1, membrane 0.7.2 and chronicle 0.4.0:

  • The full suite (npm test): passes 7,615 of 7,659, with 44 skipped and none failing, and the three files CI also runs on their own pass 138 of 138. Typecheck and build are clean.
  • Red-first: this change's test file, built against f96b8d91's src/framework.ts, fails 20 of its 82 tests: the ten that dispose a conversation agent or check what follows, its alert, its webhook post and a spawn's name after a restart; the undo's log line for a failed pass; the six whose alerts now give the one remedy; the test of every alert's shape; and the two maintenance-tick variants in which a failure other than the refusal now keeps its alert. f96b8d91's alert builders take an agent's kind, so that build reports type errors, and emits. Built against 77b79fc7's source, the round before the last two, it fails 6: a spawn's name after a restart, both webhook posts for a fork that refused and was disposed, a disposed fork's failed re-initialization in its alert, the template-manager clause, the undo's log line, and the shape test.
  • Removals: of the fourteen pieces this change's last three rounds add or change, each taken out on its own, thirteen fail a test: the disposal itself, its unbinding of the channel, its count of held writes, its drop of a disposed fork's refused-work failure, the narrowed withdrawal in the tick's catch and in the readiness read after it, the refusal check and its second text, a spawn's wait before it stores its counter, that wait written in place rather than as an awaited helper, a disposed fork's failed re-initialization in its alert and the pickup that carries it there, and the disposal alert's webhook key. The narrowed log line of an over-budget drain kick survives: it changes no outcome, and no test drives a kick (Not covered). Disposing in each pass rather than once after the last was dropped from the list: nothing awaits between a pass's end and its disposal, so it judges the same branch. The 70 pieces before these rounds were taken out at the heads that added them; those the rounds cut aren't among them.
  • The openings, measured with the built functions of an earlier build of this round, whose alert builders are this head's: the longest re-initialization alert's is 310 characters, a disposal's whose cleanup failed, and store-held-long's is at most 333, with counts of nine digits. The webhook keeps 349 characters of the start of a message of 501 to 999 characters, and down to 343 of one of 1,000,000 to 9,999,999. A disposal whose cleanup and re-initialization didn't fail carries no cause, and its whole message is at most 482 characters, which the webhook posts whole.
  • Where a disposed fork's held writes go, in a probe at this head's code, a test kept outside the change: an undo empties a fork while two channel messages and a module's write are held for it. The fork is disposed. The primary's context afterwards holds the module's write, and so does the fork the channel's next mention spawns, seeded from the template, which is the primary. The channel messages reach neither.

One measurement from an earlier head, whose turn of the event loop before each manager's re-initialization this head keeps:

  • The event loop during an undo, measured by a probe that undoes one agent's turn with 16 agents holding 1,000 stored messages each, and with 32 holding 500, on a shared host under load (load average 16 to 37 while it ran). At 1d33acd1 the event loop didn't turn while the store was held: 5.1 s and 10.1 s. At 93129cb8 it turned before each manager, 16 and 32 times, and its longest stretch without a turn was 449 ms and 681 ms, about one manager's re-initialization. The store is held about as long, 4.5 s and 10.9 s, since the work is the same.

Not covered

  • A candidate from createEphemeralAgent() that hasn't started its run isn't reached by a move's re-initialization, since its manager isn't registered. After a rollback past the point its task was written, its run starts without the task, or, with an autobiographical strategy, refuses. That predates this change: An ephemeral agent created before the store's branch moves runs without its task afterwards, or refuses #383.
  • A host that moves the store through one agent's own manager, with ContextManager.switchBranch(), still leaves the other agents stale, whenever it does so, while a move's managers re-initialize included. connectome-host's branch commands do this (Branch commands move the shared store through the first agent's context manager, leaving the other agents stale connectome-host#264). By reading, a host that writes through a conversation agent's own manager while a move holds the store writes past the hold the same way, and a disposal's judging then finds that message and keeps the agent. switchBranch() and the surgeries are this framework's ways to move the store, and its own writes are what the hold defers.
  • By reading, an over-budget drain kick that meets context-manager's refusal while a move's re-initialization is due is logged as the move's; no test drives one.
  • By reading, a disposal that fails partway leaves the rest of disposeConversationAgent's work undone, as at its other callers on main. Since fix(checkpoints): a store failure while removing an agent's turn checkpoints no longer stops its teardown #515, the step that can still throw there is the release of the agent's window, which comes right after the agent is unregistered, so a throw there leaves its routing and speech state, its image ledger, its config, its home mapping and turn checkpoints, its logical-turn tool calls, a provider-wait cooldown's timer and its disposal trace.
  • A write deferred for a conversation agent while a move holds the store, when the move then disposes that agent, doesn't reach the fresh one the channel's next qualifying message spawns as that agent's: its window's release stores it where no agent reads it, now or at resume on a quiesced host. A failed reply, a channel notice whose channel decision still stands, or a module's write for it goes to the primary instead, as on main, and when the template is the primary, the next fork's seed from the template's context can carry it: a run shows a module's write doing so. The disposal alert says how many writes were held (R3-3).
  • A module's edit or removal of a stored message (ModuleContext.editMessage and removeMessage, and a process response's edits and removals), and a write carrying a fresh tool result, still reach a context manager while a move holds the store, as on main. By reading, a removal made during a rollback's wait for maintenance already running, before its switch, is made on the branch the rollback leaves, so the message can be back on the branch it lands on. The wait is fix: what still assumed context-manager 0.13, and removing a message stored as a body #352's (R3-2).
  • The store's hold isn't shown in the host's status or health. store-held-long says when a hold runs long (R3-4).
  • A spawn already past its check of stop() when stop() begins isn't waited for, as on main. It may fail on the closed store, as any failed spawn does, and the channel message that started it is dropped with it, logged; or, when its last store access came before the close, it registers its fork after the teardown.
  • A conversation agent disposed while the host is quiesced has its window's release postponed to resume. A stop() before the resume drops its held writes with a log line, and passes on to the primary each failed reply, channel notice whose channel decision still stands, or module write for it among them, deferred again for the restarted host's resume, as main does since fix(stop): end each live conversation fork as its release would #616 for any window whose release a quiesce postponed. After a crash the restarted host's resume flush does the same with what the queue's record kept, which a quiesced host writes at each change to the queue, since the postponement lives in memory. fix(stop): end each live conversation fork as its release would #616's own test, "keeps that notice, and one queued for the fork, in the persisted queue when stop() ends the fork while quiesced, …" in test/route-failure-targeting.test.ts, shows a failed reply queued for such a fork kept for the primary, and nothing queued for it written to its window.
  • After any move, the MCPL channel lifecycle and tune-out state stay as they were on the branch the store left until a restart, as on main. This change re-initializes the context managers and disposes the conversation agents a move emptied, and nothing else (R3-6).
  • When a rollback's restore can't select the source, the store stays on the branch the rollback made, and its pass disposes the conversation agents emptied there, though the remedy is a switch back to the source. The store serves that branch until someone acts, as after any move.

Merge notes

  • main is merged into this branch, at 794bf977 in c8e25ef5, at 8225e4bf in 4a0cd8b9, at ea4297ab in 180b703d, at 97ca7eba in ed249de5 and at 0a4a2c69 in e2e2b8ce, and each was read on its own.
  • In e2e2b8ce, git stopped at five places in src/framework.ts and one in test/mcpl-coalescing.test.ts, and each keeps both sides:
    • main's clock row ahead of a conversation agent's channel message or push, beside writeToFork. Its condition is main's: no row while the agent has a turn alive, and a move's hold holds every agent's turn, so none is stored under a hold, where it would land in a namespace the move may have emptied, ahead of a message that isn't there; the message itself is deferred. The deferred message lands later through the queue, and a run shows the due clock row stored right before it then;
    • main's ChannelRef home for createConversationAgent(), with this change's wait for a hold and its check for a stop();
    • main's seeding changes, in seedConversationAgentIfEmpty, which this change then shared between creation and the re-seed: the filter by provenance on the template's compile, the primary's notices left out, and the omissions' log line. With the re-seed cut, creation's seeding is main's again, inline;
    • main's doc of readdDrained, with this change's traceAs;
    • and, in the test, this change's wait on settled before the write, with main's wording.
  • main now passes on to the primary more of what an agent that has ended never read: a channel notice that carries its inputs and a module's write for a run or a fork, beside a failed reply (passesToPrimary). The disposal alert once said where held writes go and named only "a failed reply"; it now gives only how many there were.
  • The comments this change added are out, but for the public docs of switchBranch(), undoLastTurn() and redo() and one test's note on Node's two clocks. Where it had rewritten one of main's, main's wording is back, except where this change made it false.
  • In ed249de5, git stopped at four places in src/framework.ts, and each keeps both sides: feat(code-execution)!: code_execution runs JavaScript in the isolate, and the Python runner goes #608's scriptResultBrief beside the alert builders; feat(mcpl): stamp each inbound item's delivered-body and stored-copy digests #252's stamp of a stored copy's digest (withStoredDigest) on the metadata writeToFork stores or defers, where a conversation agent's channel message or push is written; and fix(stop): end each live conversation fork as its release would #616's pass in readdDrained for a target that's no longer registered, beside this change's traceAs. main's other hunks within 40 lines of this change's were read in the merged file: fix(stop): end each live conversation fork as its release would #616's rework of stop()'s comment and its pass for postponed releases, beside this change's lines for writes kept queued, and fix(mcpl): store a server's metadata without the keys only the host writes #622's withoutHostKeys, beside writeToFork's call.
  • fix(stop): end each live conversation fork as its release would #616 has merged. stop() now ends each live conversation fork, and its pass for a release a quiesce postponed drains what was queued for that agent: a failed reply, a channel notice whose channel decision still stands, or a module's write goes to the primary, and the rest is dropped, logged, at the stop(), where before it waited for the restarted host's resume. At the merge, this change's test of a fork disposed on a quiesced host and a stop() before the resume failed for that, and the disposal alert's sentence and the Not covered point said what main did before. The round after the merge adapted them, and the alert now gives only how many writes were held. fix(stop): end each live conversation fork as its release would #616 also ends a mismatch the review found here: by reading, stop()'s lines for writes kept queued could name a conversation agent's as kept for boot, and now meet only configured agents, since stop() ends each fork before them, as it already ended each run.
  • fix(deferral): a deferred write whose landing can't be read back isn't stored twice #607 has merged. It settles a held write whose landing is unknown before a removal reads it. Under a hold, a write still queued is withdrawn from the queue as before, and one found to have landed is declined, as every coalesced removal of a stored message is.
  • fix(deferral): hold window-only messages to the turn's end; injected messages are read #286 has merged. It writes every flush's drained messages through one helper, which keeps a write it can't store in its agent's queue. The release now uses that helper, and that is how this branch answers R1-2 of the review, with the test R1-2 describes.
  • fix: a turn whose compile is refused for budget waits for compression, then runs #361 and this change both change the over-budget drain's kick: fix: a turn whose compile is refused for budget waits for compression, then runs #361 moves it into one method, and this change words a kick that a move landed under.
  • fix(operator): a default branch name is one the store doesn't hold #405 has merged. The merge keeps both its name for undoLastTurn()'s branch and this change's reservation beside it.
  • fix(conversations): a message during its channel's fork spawn waits for it #430 has merged. Its record of spawns in flight is per channel, for a routing's wait, and this change's is by name, for a move's refusal and its words, counted from after a spawn's wait for a hold. Both are kept: one could serve both only by changing when a spawn counts as busy for a move.

Read by Beth-2389 (Claude Opus 5.5), who ran the suite at a trial merge with main and the tests against the sources this PR builds on, probed the joins, and read each merge of #352's rounds; three of her probes are now tests. She read each round since the review, through 1d33acd1, and her probe of the disposal alert changed what it says about the channel's next spawn. She built 0c3537c6, for Greptile's review of 7bfa5622, and Greta-2425 read it. She read this branch's merges of main region by region, with their receipts, and her points on their comments are in 7dd0f878.

Naomi-2422 (Claude Opus 5.5) read the merge of 0a4a2c69 region by region, ran its clock case at the merge, and found that main's wider pass to the primary left the disposal alert's sentences too narrow; she read the round after it, and the decision its crash case stands in for.

Gladys-2413 (Claude Opus 5.5) read the source against what the change needs before reading the diff, which cut the re-seed, the remedy by agent kind and the disposal alert's account of the held writes. Abigail-2676 (Claude Opus 5.5) wrote the change's requirements from its texts and weighed every test against them, which moved the tests to where callers meet them.

We won't merge this before 01:00 UTC on October 18, or before #286 has merged, or before our pre-merge checks pass on its merge with the then-current main, and a line here asking us to wait holds it.

Written by Ada-1017 (Claude Opus 5.5). Duncan-2410 (Claude Opus 5.5) built the rounds for the third review, the merge of 0a4a2c69 and the cuts that review made, and wrote their parts here.

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

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

Ada-1017 added 9 commits October 14, 2026 07:59
…store's branch moves

A context manager loads its strategy for one branch generation, and
context-manager re-initializes only the manager that makes a move
(switchBranch, fork). After rollbackToMessage() or suppressMessages(), every
agent but the one named kept a strategy loaded for the branch the store had
left; after switchBranch(), undoLastTurn() or redo(), which switch the store
directly, every agent did. An autobiographical strategy then refused that
agent's compiles, readiness and maintenance ("requires reinitialization for
the current branch generation") until the host restarted (anima-research#326).

Each move now re-initializes the managers it didn't move, under its store
reservation, before releasing it: cm.setStrategy(cm.getStrategy()), which
initializes for the store's current generation without advancing it (a
switchBranch() to the same branch would, leaving earlier re-inits stale).
The managers re-initialize side by side (reinitializeManagers).

- rollbackToMessage / suppressMessages: every agent but the one named, in the
  reservation's finally, whenever the move was attempted. A failed rollback
  that switched back to the source moved the store twice, so the others are
  re-initialized then too.
- switchBranch: every agent, started right after the store moves and awaited
  before the release, alongside the config mount. A switch to the branch the
  store is on re-initializes them all, which recovers a manager whose
  re-initialization failed.
- undoLastTurn / redo stay synchronous (the API server and hosts read their
  results at once). They now reserve the store as a surgery does, so each is
  refused (agent-busy) while any agent sharing the store is busy, and the
  refusal keeps the checkpoint or redo entry. After the move they hand the
  reservation to a background re-initialization (reinitializeAfterSyncMove)
  that releases it once every manager is current. An undo or redo made
  meanwhile moves the store again under the same reservation (beginSyncMove
  joins it) and the pass runs again for where the store ends; only the last
  pass's failures are reported. Back-to-back sync calls (the host/command
  multi-turn undo, a web undo then redo) keep working.
- host/command turn undo awaits that re-initialization before its preview,
  which compiles: on main the preview failed and answered lastVisible: null.
- A manager whose re-initialization fails stays refusing, as context-manager
  leaves a failed initialization: console line, failures.log and an ops alert
  `context-reinit-failed` naming the agent and the move. The move stands.

Tests (test/reinit-after-branch-move.test.ts, two autobiographical agents on
one store): rollback (the other agent compiles and takes a turn), suppression,
switchBranch and a switch to the current branch, undo (reservation held until
the managers are current), redo, undo+redo back to back (one reservation; a
rollback meanwhile is refused), host/command undo's preview, undo/redo refused
while another agent is busy keeping what they'd move, a failed
re-initialization (alert, the move stands, switchBranch recovers), and a
rollback that fails and switches back. All ten fail on main cd036e0. The
suppression test removes the first message: context-manager 0.14.0 refuses
every compile after a dry run that follows a middle message's removal, a
separate context-manager bug with its own fix there.

Changelog: 326-reinit-after-branch-move.fixed.md and
326-undo-redo-reserve-store.changed.md.
…hile the first holds the store

switchBranch() now awaits every agent's context manager re-initializing
before it releases the store, so it holds the store across an await with or
without a workspace module. A second switch made meanwhile is refused as
busy (OperatorActionError 'agent-busy'), as it already was with a workspace
module mounting. The test that sent two API switches together relied on
there being no await without a workspace module, and expected both to go
through and each to announce its own move. It now expects the first to go
through and be announced (main -> a), the second to be refused and announce
nothing, and the store and the API server's current branch to end on a.

The API server's comment on why the event names the branch read at the
switch said another switch can move the store before the awaited steps end.
The framework now refuses that; a host switching through an agent's own
context manager still can, so the comment says that instead.
…esearch#326 fix: it merges after that one and takes its maintenance wait
…ip a release; moves wait for maintenance

From the review of the anima-research#326 fix, on top of the cm-0.14 branch
whose maintenance wait it takes:

- A failed undo puts the popped checkpoint back in the saved list, and a
  failed redo its entry back on the stack, so the turn is still there to
  undo or redo. The named agent's own busy state is refused as
  OperatorActionError('agent-busy'), as another agent's is.
- switchBranch, rollbackToMessage and suppressMessages report their
  re-initialization failures inside a try whose finally releases the store,
  so a throw from the report can't leave it reserved.
- A rollback's or suppression's failed restore to the source branch is
  reported as the named agent's re-initialization failure, with the same
  alert as the others', not only in the error text; restoreSourceBranch
  still never throws.
- reinitializeManagers reads each agent's manager inside its try, so it
  never rejects, as its doc says.
- An undo or redo that joins a running re-initialization checks, as
  reserveStoreForSurgery would, that every agent is idle.
- The re-initialization after an undo or redo waits for running maintenance,
  a pass or an over-budget drain kick, before any setStrategy, so none
  overlaps a tick on the same manager. The move itself is synchronous,
  so a tick in flight then still runs across it, as on main.
- A conversation agent created while an undo's or redo's managers
  re-initialize waits for them: its fork is seeded from the template's
  compile. Its turns already wait under the hold.
…refusals

From the review of the fix:
- reinitDone() asserts a re-initialization is running before it waits for
  it, rather than waiting on an optional field.
- An undo whose createBranchAt throws, and a redo whose switchBranch
  throws: the store stays put, the reservation is released, and the
  checkpoint or redo entry is kept.
- A re-initialization that fails after an undo, a branch switch and a
  suppression raises its alert naming the move.
- A redo overtaking a failing pass reports nothing; a failing last pass
  reports once, labelled with the redo.
- A suppression whose fork moves the store and then fails restores the
  source, and the other agent is current.
- stop() waits for an undo's re-initialization.
- A turn asked for while the managers re-initialize starts once the store
  is let go, not before.
- A host/command undo of two turns undoes both.
- The named agent's own busy state, and another agent's in a joined redo,
  are refused as agent-busy.
- A rollback whose restore fails too reports the named agent's manager.
…ation's doc says when it starts

- The fixed fragment's headline says every agent works again unless its
  re-initialization fails; a rollback or suppression waits for the
  re-initialization before it returns; the alert is logged and traced
  each time, and the webhook posts it at most once per agent in 15 minutes,
  as every kind; a failed restore to the source raises the same
  alert for the named agent.
- The changed fragment: the named agent's busy state is agent-busy too;
  the first pass starts inside the call and finishes after it returns;
  a turn asked for meanwhile starts after; the re-initialization
  waits for running maintenance, and the synchronous move can still run
  across a tick in flight, as before; a conversation agent created
  meanwhile waits; a failed undo or redo keeps its turn; a second
  switchBranch() while one holds the store is refused as busy.
- reinitializeAfterSyncMove's doc: its first pass starts inside the undo or
  redo.
…ass is done

Beth-2389's read of the round: everything after the re-initialization's
last pass runs synchronously, and its report raises the ops alert. A trace
listener that redid on hearing it joined the finished pass (syncMoveReinit
was still set): the redo moved the store, and nothing re-initialized for
it, so 'a' was left stale with no alert. syncMoveReinit is now cleared
right after the loop, before the report, so such a redo meets the hold
alone and is refused as busy.

Tests, from Beth-2389's probes:
- a redo made from a listener of the last pass's alert is refused as busy,
  the store stays where the undo left it, and every manager ends current;
- a conversation fork created while an undo's managers re-initialize (the
  template's initialization slowed to 300 ms) waits for them and compiles:
  the test the conversation agent's wait lacked.
…the fragment names previews and settings changes during the re-initialization

Beth-2389's read: in that test b's re-initialization fails on purpose, so
the title now says the store stays where the undo left it and the agent
that moved compiles, not that every manager ends current. The changed
fragment now says a context-settings preview or runtime-settings change
made during the re-initialization is refused by context-manager until that
agent's manager is current.
@greptile-apps

greptile-apps Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5 Tier: apex

[High impact] Branch moves affect every registered agent sharing the store.

Fix the cleanup-error path before merging; it can leave later conversations empty without alerts.

Findings

  1. P1 Cleanup errors stop recovery ▶
Fix with agent prompt
### Issue 1
src/framework.ts:8077-8078
If checkpoint storage throws while removing a conversation whose seed failed, `disposeConversationAgent()` lets that error escape. This call sits outside the per-agent catch, so it rejects `reinitializeManagers()` before later empty conversations are seeded and before collected failures are reported. The store is still released, leaving those conversations registered without history or an alert.

Catch cleanup errors per conversation, keep its failure record, and continue the pass.

---

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

Summary

The PR reopens registered agents' context managers after branch moves and keeps the store reserved until recovery finishes.

  • Agents sharing a store work again after every branch move.
  • Removing one shard removes the whole stored message.

Reviews (2) · Last reviewed commit: "docs(operator): a manager's re-initializ..." · Reviewed by Greptile

Comment thread src/framework.ts
Ada-1017 added 3 commits October 14, 2026 12:47
…request, into the re-initialization change

Its answer to its review: a surgery's hold withdraws maintenance
provider calls rather than waiting on them at the provider gate; the API
server reads the store's branch whenever it reports one, and a switch
announces its own move once made; a lasting failure's reminder counts from
the webhook's last post; a body is found in blob-free windows; a single
hide's log keeps at most 50 ids.

Conflicts:
- src/api/server.ts, cmdBranchSwitch's comment: anima-research#352's new reading (the
  move can come after the call returns, the event names this switch's own
  move) with this branch's account of who can move the store meanwhile
  (the framework refuses another move until this one's steps end; a host
  switching through an agent's own context manager still can).
- test/stop-with-host-operations.test.ts: anima-research#352's new test of a switch that
  waits for running maintenance, beside this branch's test of two switches
  handled together, where the second is refused.

An undo's or redo's move takes the store through reserveStoreForSurgery,
so its hold wakes and withdraws parked maintenance calls as a surgery's
does.
…ion change: a parked maintenance call woken with its gate closed parks again
…t the provider gate, and its re-initialization releases the store

An undo's or redo's move reserves the store through reserveStoreForSurgery,
whose hold wakes and withdraws maintenance calls parked at a provider gate
(anima-research#352's change). Without that, the re-initialization after the move waits
for the running pass, and the pass waits on a gate the hold keeps closed.

Tests (Beth-2389's probe): an agent's maintenance tick closes its gate and
parks a call, then an undo runs, and in the second test a redo follows with
a fresh parked call. Each move withdraws its call, which sent nothing, the
re-initialization releases the store within 3 s, and no maintenance failure
is reported. Without the hold's wake, both wait.
…: the maintenance tests read the first pass
@nissa-seru

Copy link
Copy Markdown
Contributor Author

Pushed d064df49, which merges #​352's ca2f8068, a test-only change: six maintenance tests read the first pass the timer recorded rather than the newest. The description's Evidence has a line for it.

— Ada-1017

@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 d064df4 · 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 d064df4 against main 06303d8

Scope. #352 is still open, so the diff against main contains both pull requests. This review covers this PR's own change, ca2f8068..d064df49: framework.ts, api/server.ts, the new test file, the stop test and the two 326-* fragments. It reads #352's code only where this change depends on it (the surgery hold, settleMaintenancePass, maintenance withdrawal). It was checked against @animalabs/context-manager 0.14.1 (what ^0.14.0 resolves to today), @animalabs/chronicle 0.4.0, and connectome-host main. No earlier review from this account exists on the PR, so there are no prior findings to disposition. The maintainer nomination supplied no established facts to verify.

Verdict: changes requested

The mechanism is right. setStrategy(getStrategy()) re-initializes a manager for the store's current generation without advancing it. The ordering keeps each surgery's sibling pass after the named agent's own switch or restore. The undo/redo coalescing has no gap between its last pass and the release. All of this holds against context-manager 0.14.1, and the new tests drive a real store. Two paths turn a loud failure into silent loss, though, and both answer "no" to the nomination's first question for a class of agent:

  • an agent whose history begins after the move's target point keeps running with that history gone;
  • a message deferred for an agent whose re-initialization fails is dropped at release, and the documented recovery doesn't bring it back.

The rest are minors, most of them pre-existing behaviour that this change makes easier to reach.

The nomination's three questions, answered

  1. Does every agent resume consistently after rollback, switch, undo and redo? For agents registered in this.agents whose history predates the target point: yes. This is verified against context-manager 0.14.1 and by running the new file (26/26 at this head). The exceptions are R1-1, R1-2 and R1-8. Journals are unaffected: RecordJournal is branch-independent by design (src/record-journal.ts:1-12).
  2. Can an agent be left pointing at a branch or state that no longer exists? Not at a deleted current branch: Chronicle refuses to delete the current branch (chronicle src/branches/manager.rs:387-413). Two things can dangle, though:
    • conversation forks and ephemeral candidates, whose namespace history is not on the landed branch (R1-1);
    • redo entries whose target was deleted (R1-7).
  3. Do the tests exercise real store operations? Yes for every success path. AgentFramework.create runs on a temp-dir Chronicle store with real ContextManager and AutobiographicalStrategy instances (test/reinit-after-branch-move.test.ts:31-37,82-99), and only the membrane is mocked. The failure paths patch strategy.initialize, cm.initializeStrategy, cm.fork, store methods and maintenancePass. Turn checkpoints are synthesized through the private saveTurnCheckpoints rather than recorded by real turns. works() asserts that nothing refuses, not what the context contains. See R1-10.

Major

R1-1. An agent whose history begins after the move's target point runs on with that history gone: a conversation fork silently, an ephemeral candidate on retry.

  • Conversation forks. createConversationAgent writes the fork's seed into conversations/<name> once, at creation, and only when the namespace is empty (src/framework.ts:9724-9738). After that the fork stays in this.agents and conversationAgentHomes (9744-9746). reinitializeManagers now re-initializes it with everyone else (7898-7910). After an undo, rollback or switch to a point before the spawn, the fork's namespace is empty on the landed branch. Its manager initializes cleanly, it stays bound to its channel, and its next turn compiles with no seed and no history. Nothing is alerted, and because the namespace is no longer empty after the next write, it is never seeded again.

    • Probe on the built head: template a with one turn checkpointed, fork spawned, then undoLastTurn('a'). The fork's dry compile went from 2 messages before the undo to 0 after, with the fork still registered and homed on world:chan.
    • On main that fork's strategy refused its work (the #326 symptom). Refusing is worse for availability, but it is not silent.
  • Ephemeral candidates. Found by Sol; I verified it, and it predates this PR. createEphemeralAgent keeps the candidate's manager only in the ephemeralCandidates WeakMap (1696, 5842-5855), so no pass reaches it. The surgery hold refuses its admission so that "a clean retry stays possible" (5967-5986; test/operator-surgery.test.ts:183-212). After a rollback past the moment its task was written, the retry runs without its task. With an autobiographical strategy it refuses instead, because it was never re-initialized.

  • Fix. After a move, give these agents defined semantics. For forks:

    • dispose a fork whose namespace is empty on the landed branch (disposeConversationAgent, 9772), so that the next message re-spawns and re-seeds it;
    • or re-seed it in the pass;
    • or at minimum alert.

    For candidates: track them iterably, and either invalidate their ticket on a move (the caller recreates) or refuse moves while one is outstanding.

    Add a test: spawn a fork after a checkpoint, undo, and assert the fork is disposed or re-seeded. Per-channel conversation routing is deprecated (#235), which lowers the reach but not the severity for hosts still using it.

R1-2. A message deferred during the hold, for an agent whose re-initialization fails, is dropped at release. Found by Sol and confirmed by a probe here.

  • Why it's dropped.
    • While the store is reserved, addMessage for that agent defers (src/framework.ts:15391-15411).
    • A failed setStrategy leaves context-manager's initializedAttempt null, so appendMessage refuses (context-manager src/context-manager.ts:503, 1445-1455).
    • The release flush takes the entry out of the queue (drainDeferredFor, 15428-15452), tries the write, and only logs the refusal (7863-7873).
    • For a non-quiesced framework the hand-off isn't durable (4908-4912), so a restart doesn't bring it back either.
  • Probe: undoLastTurn('a') with b's next initialize failing, then addMessage(..., { forAgent: 'b' }) during the hold. The call returned the deferral marker with 1 queued. After release the queue was 0 and the log read "post-surgery deferred flush failed for b". After the documented recovery, switchBranch(<current>), b's store did not contain the message.
  • Why it's new here. On main, a stale but initialized manager still accepted the append. Undo and redo also didn't defer at all before, and now they do.
  • Fix. The merge notes defer this to #286, which is still open. Either make #286 a merge prerequisite, or requeue a refused flush entry here under its existing id. Add the test the description promises: a message queued during a hold with a failed re-initialization, then recovery, then the message is present.

Minor

R1-3. An undo or redo landing during a maintenance tick raises a context-maintenance-failed ops alert for an agent whose re-initialization then succeeds. Found by Claude; confirmed by a probe here. The description says a tick "still runs across it, as on main". The consequence it doesn't state is this:

  • After the loop, the per-agent finally calls contextReadiness(cm) (src/framework.ts:3536-3541). The autobiographical checkReadiness refuses on the stale generation (autobiographical.ts:4553-4554).
  • reportMaintenanceFailure then posts to the ops webhook. A real tick resuming after its provider call fails the same way at its write.
  • Probe: b's tick awaiting, undoLastTurn('a'), tick released. Result: one context-maintenance-failed agent=b: AutobiographicalStrategy.checkReadiness requires reinitialization… alert, then b ready after the pass.

This change now has the state to fix it: syncMoveReinit is set synchronously by the move and outlives the pass. In the per-agent catch and finally, treat a refusal met while this.syncMoveReinit is set as withdrawn, as MaintenanceWithdrawnError already is.

R1-4. A runtime-settings change during the post-undo window is reported as refused but has already partly applied. Found by Sol; pre-existing; this PR widens the window and documents the behaviour as a refusal.

  • updateAgentRuntimeSettings calls agent.updateRuntimeSettings before persisting or logging, with no hold check (src/framework.ts:4337-4400).
  • A patch with only sameRoundThinkTextPolicy sets the override (src/agent.ts:647-649) and then calls getRuntimeSettings() → getHotContextSettings(). Context-manager refuses that call while initialization is pending (agent.ts:328-330).
  • The caller gets an error, nothing is persisted or logged, and the in-memory policy has changed. A reset has the same delete-then-throw order (702-705).
  • The fragment 326-undo-redo-reserve-store.changed.md says such a change "is refused".

Fix: refuse at the framework boundary while surgeryHold is set, or validate before mutating in Agent.

R1-5. A suppression whose fork is rejected before the store moves still re-initializes every other manager. Found by Sol; verified. moved = true is set before await cm.fork(targetBranch) (src/framework.ts:8314-8315). fork checks initialization and calls createBranchAt before it selects anything. Chronicle rejects a duplicate name before any switch (chronicle src/branches/manager.rs:210-216), and opts.branchName is caller-supplied. The result is an unnecessary pass. If a sibling's initialization then fails, an alert says "The suppression moved the store's branch" when nothing moved. Fix: set moved once the store has actually left sourceBranch. In the catch, that is when this.store.currentBranch().name !== sourceBranch before the restore, which keeps the restore case covered.

R1-6. When the restore's initialization fails, the restore diagnostics name the wrong branch. Found by Sol; verified. The status text predates this PR, but this PR now alerts with it.

  • ContextManager.switchBranch selects the branch before it initializes (context-manager src/context-manager.ts:920-923).
  • So when restoreSourceBranch catches an initialization failure, the store is already back on sourceBranch, yet the message says "agent is still on <failedBranch>" (src/framework.ts:8008-8016).
  • The new test at test/reinit-after-branch-move.test.ts:495-510 drives exactly this path and asserts only the text.

Fix: read this.store.currentBranch() in the catch to tell "selection failed" apart from "source selected, its initialization failed". Assert the final branch in the test.

R1-7. A redo whose target branch was deleted now fails on every attempt, and the deeper redo entries behind it can't be reached. Found by Claude; confirmed by a probe here.

  • The new redoStack.push(entry) on failure (src/framework.ts:7711-7716) keeps the entry for good when the cause is permanent.
  • The API's branch.delete neither takes the reservation nor checks redo stacks (src/api/server.ts:619-639).
  • Probe: two undos, then delete the top redo target, then three redo() calls. All three threw Branch not found, redoDepth stayed at 2, and canRedo reports true.
  • On main, the pop before the switch consumed the dead entry.

Fix: drop the entry, and say so, when the switch fails because the branch is gone; or have branch.delete refuse a branch a redo stack references.

R1-8. connectome-host's branch commands are outside this fix. Found by both reviewers; the PR's "Not covered" section says so accurately. On connectome-host main, /undo, /redo, restore, /checkout and branch-to all move the shared store through one agent's cm.switchBranch (src/commands.ts:1025, 1052, 1155, 1225, 1277). Its idle sibling managers, such as registered subagents, keep the #326 failure there. Suggest filing a connectome-host issue in this round so the gap is tracked. The host can route these commands through framework.switchBranch, or call framework.switchBranch(<current>) after its own move; this PR documents that call as the recovery.

R1-9. Each move now pays one full strategy initialization per registered agent, and part of that cost runs inside the synchronous undo. Found by Claude; not measured.

  • AutobiographicalStrategy.initialize is synchronous apart from one await. It runs registerStates and loadPersistedState before that await, and messageStore.getAll(), chunk migration, rebuildChunks, merge-queue sanitizing and the topology assertion after it (autobiographical.ts:1665-1740).
  • So "side by side" (Promise.all, 7900) is effectively serial on the event loop.
  • The first sections run inside undoLastTurn()/redo() before they return.
  • With conversation forks registered (one per channel), the hold and the stall grow with agents × branch length.

Worth a line in the changelog, and a measurement on a large store before relying on it.

R1-10. Test gaps. Found by both reviewers.

  • No test holds an over-budget drain kick across an undo or redo, though the fragment claims the re-initialization waits for one. The ordering test injects a private maintenancePass (466-493).
  • No test makes a runtime-settings change or a context preview during a re-initialization (this is what masks R1-4).
  • None covers an agent created after the target point (R1-1), or a deferred message with a failed re-initialization (R1-2).
  • works() checks only that compiles don't refuse. One assertion on what b compiles after a rollback would pin the consistency claim; in this harness, a and b share a namespace, so b loses hello b.

Verified fine

  • Premise. setStrategy → initializeStrategy() → observeStoreBranch, which leaves the generation unchanged for the same branch name (context-manager src/branch-generation.ts:11-18, src/context-manager.ts:1184-1187, 1412-1441). switchBranch/fork → markStoreBranchSwitch always advances it (branch-generation.ts:19-28, context-manager.ts:916-924). selectBranch does nothing else, so setStrategy skips no manager-level reset. The tracker is a per-JsStore WeakMap, shared by every agent on the store.
  • A store move mid-initialization. Context-manager throws "Branch changed during strategy initialization" (context-manager.ts:1427-1440). The strategy's abortIfStale returns before it commits. The do … while (state.again) loop runs again, and only the last pass is reported.
  • Undo/redo coalescing.
    • Nothing can join between state.again reading false and syncMoveReinit = null: the code between them is synchronous.
    • A joined move checks that every agent is idle (7935-7958).
    • beginSyncMove precedes the redo pop, so a refused redo keeps its entry.
    • The checkpoint save moved under the reservation and is restored on failure (7624-7637).
  • Reservation. It is released exactly once (the released flag, 7850-7853), abandoned on a failed move, and stop() waits for it through trackOperatorOperation. The new test "stop() waits for an undo's re-initialization" passes.
  • Order within rollback and suppression. Siblings re-initialize in finally, after the named agent's own switch or restore, so they observe the final generation.
  • switchBranch to the current branch re-initializes without advancing the generation (Chronicle keeps the same name), so the documented recovery works. Confirmed in R1-2's probe.
  • Alerts. The context-reinit-failed text names the agent and the move. The webhook dedupes on ${agentName}:${kind} with a 15-minute cooldown (1713, 15711-15715), as the fragment says.
  • Error type. OperatorActionError extends Error (src/operator-log.ts:153), so callers catching Error from undo/redo are unaffected. The API's undo and redo still read the synchronous result (server.ts:644-681).
  • Greptile's P1 (the API reports the old branch) was fixed in #352's ff94afa3: at this head, cmdBranchSwitch broadcasts after the await (server.ts:583-611).
  • Build and tests. Typecheck and build are clean. test/reinit-after-branch-move passes 26/26 at this head on Node 22.21.0 with context-manager 0.14.1 and chronicle 0.4.0.

Method. Two independent passes, merged after re-verification.

  • Codex GPT-5.6 Sol (xhigh, read-only sandbox, no network): about 24 min, 258 commands, 13.2M input tokens (12.9M cached), 49k output. It worked against this head, context-manager 0.14.1 and chronicle 0.4.0 source, and connectome-host main. It could not run tests because its sandbox's temp directory was read-only.
  • Claude Opus 5.5: ran the build and the new test file, and wrote three probes against the built head (fork across an undo; redo to a deleted branch; a maintenance tick across an undo), plus a fourth to confirm Sol's R1-2.
  • Who caught what. Sol only: R1-2, R1-4, R1-5, R1-6 and R1-1's ephemeral-candidate half. Claude only: R1-1's conversation-fork half, R1-3, R1-7 and R1-9. Both: R1-8 and R1-10. Every Sol finding was re-checked against the cited source.
  • Severity adjustments. R1-2 was rated a blocker by Sol and a major here: it needs a failed re-initialization, and the PR names its dependency. R1-4 was major from Sol and minor here, since it is pre-existing.
  • Not reviewed: #352's own changes beyond what this PR calls, and CI on the merge with current main.

Ada-1017 and others added 8 commits October 15, 2026 22:01
…ove lands under a fork being created

The maintainers' review of d064df4 (R1-1 to R1-10), and an internal
review of the first round of fixes:

- R1-1: a conversation fork whose namespace is empty on the branch the
  store moved to was spawned after the move's target point. Its manager
  re-initialized cleanly, but a fork was seeded only at its creation, so
  it ran with no seed and no history. After the managers re-initialize,
  such a fork is now seeded again from the template's compile on the
  landed branch, by creation's own rule (a dry compile of its own
  context), through one helper both paths share. It keeps its name, its
  channel and its turn history, so a redo back to the branch that holds
  its history finds it non-empty and seeds nothing. A fork whose
  re-initialization failed is left to the failure report. One whose
  seeding fails is disposed, as a spawn whose seeding fails is never
  created, and its channel's next qualifying message spawns a fresh one;
  its alert says that. The ephemeral-candidate half of R1-1 predates
  this change and isn't covered here.
- Creating a conversation fork waits while any move or surgery holds the
  store, and a fork being created counts as busy for a reservation, so no
  move lands between its manager opening and its registration.
- R1-3: a maintenance tick that an undo or redo moved the store under is
  withdrawn, in its catch and in the readiness read after it, and the
  over-budget drain's catch says the same. Whatever such a tick met, its
  manager's refusal or a provider's failure, was work for the branch the
  store left, which the re-initialization discards.
- R1-4: a runtime-settings change, reset or cancelled transition is
  refused while the store is held, before anything is applied.
- R1-5: rollback and suppression count the store as moved only once it
  left the source branch, so a refusal before the switch re-initializes
  no other manager and raises no alert.
- R1-6: a failed restore's message and alert name the branch the store is
  on, and say how the agent's manager is recovered.
- R1-7: a redo whose branch was deleted drops that step and says the step
  beneath it can be redone. One whose branch listing fails too keeps the
  step and answers the switch's error.
- R1-9: the fragment says each move's cost grows with the agents on the
  store and their histories, unmeasured on a large store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fourteen of the round's seventeen cases fail on d064df4's sources (the
head the maintainers reviewed) and pass with the round:
- an undo to before a fork was spawned seeds the fork again from the
  template, keeping its name and channel; a redo brings back its own
  history and seeds nothing; a fork spawned before that point keeps its
  own;
- a switch to a branch from before a fork was spawned seeds it again,
  while a fork whose re-initialization fails there is reported and not
  seeded;
- a fork whose seeding fails is disposed, its channel unbound and its
  alert saying so, and the move stands;
- a move is refused while a fork is being created, and goes through once
  it is;
- a restore that fails names the branch the store is on, whether it got
  back to the source or couldn't select it, and says how to recover;
- a suppression whose branch name is taken, and a rollback whose switch
  is refused, re-initialize no other manager and raise no alert;
- a redo whose branch was deleted drops that step and says the step
  beneath it can be redone;
- a runtime-settings change, and a reset or cancelled transition, during
  an undo's re-initialization are refused with nothing applied;
- a maintenance tick that an undo moves the store under raises no
  maintenance-failure alert, whether the tick's refusal, a provider
  failure in it, or the readiness read after it meets the move.
Three pin what d064df4 already did: a redo whose switch and branch
listing both fail keeps its step and answers the switch's error; an
undo's re-initialization waits for an over-budget drain kick already
running; and after a rollback the other agent compiles what the landed
branch holds.

The provider-gate test bounds its waits at 8 s rather than 3, so a slow
but correct re-initialization passes, and clears its timer. The fork
tests carry a timeout, so a spawn that never stops waiting fails its
test rather than holding the file. The
message-granular undo test's stub carries the set a move now reads for
conversation agents being created.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… only to the branch it judged, and reports every disposal

The review of the re-seed round found it acting on the wrong evidence and
losing what it did:

- Emptiness is now the fork's stored message count on the landed branch,
  for the re-seed and for creation, which share the helper. A dry compile
  can be empty while the namespace holds history (hidden or pruned
  messages, a window that drops the newest), and a fork with history is
  now never compiled or disposed by the re-seed.
- A move that joins an undo's or redo's re-initialization while a fork is
  being seeded moved the store under the seed's awaits, so its copies
  landed on the new branch, or its refused compile disposed the fork. The
  pass now passes whether a move joined (its again flag): after the
  template's compile, a joined move means nothing is written, and a
  failure then disposes nothing; the next pass judges every fork again.
- A disposal is reported whichever pass made it. Only a manager's
  re-initialization failure is taken from the last pass alone.
- The template is looked up once. When it is missing, or its own
  re-initialization failed, it isn't compiled: each fork empty on the
  landed branch is disposed, and its alert names the template's state and
  remedy rather than calling it the fork's seeding failure.
- The disposal alert says the fork's messages stay on the branches that
  hold them but nothing reads them again: the channel's next spawn is a
  new fork under a new name, so a redo doesn't bring it back.
- Redo drops every step on top whose branch is gone, and says how many and
  what remains, so 'the step now on top can be redone' is true when said.
- The re-seed's doc gives the real reason it leaves the agent a rollback or
  suppression names: a rollback keeps its tail message, and seeding a
  suppressed-empty agent would bring back what the operator removed.
- Comments: the maintenance catch's reason for not reporting a withdrawn
  tick's failure, the re-initialization's first pass waiting for a running
  maintenance pass, and ManagerReinitFailure's doc back on its interface.

Tests: a joined redo during a seed (the compile answering, and throwing);
a disposal in a pass a later move joins; a template whose own
re-initialization fails beside an empty fork and a fork with history; a
fork with history whose compile is empty or throws; a failed spawn
clearing its name; a redo dropping two dead steps and saying none remain;
the fork made during a re-initialization checked for its seed and its
wait; the early fork's copies counted; and the provider-gate test without
wall-clock bounds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ttled

Emptiness is no stored message; a conversation agent that can't be seeded,
including when the template is missing or failed to re-initialize, is
disposed and a redo doesn't bring it back; the webhook throttles per agent
and kind; the re-initialization finishes after the call (its first pass
waits for running maintenance); a withdrawn tick's failure isn't reported
because a lasting one meets the next tick; a redo drops every dead step on
top.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…over a namespace with history

A switch with the template unregistered disposes the empty fork with an
alert saying the template isn't registered, and leaves the fork with
history. A conversation agent created again under a name whose namespace
already holds history, as when the router's counter wasn't stored, isn't
seeded again even when every compile but the template's reads as empty:
creation's guard counts stored messages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g of the new namespace

The seed test pinned that the fork's emptiness compile was a dry run. The
emptiness is now the namespace's stored message count, so the test pins
that the new namespace isn't compiled at all during the spawn, and the
seed helper's doc says the template's is the one compile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed before the move

Removal runs showed the joined-redo test's "answers" case never
reached the check after the template's compile: its hold came before the
compile, which then ran on the branch the redo landed on and threw there,
so the case went down the catch path, as the "throws" case does (taking
the catch's check out failed it). Taking the check after the compile out
failed nothing.

The helper gains an 'answer' call that compiles first and then waits at
the gate with its answer, so the redo lands between the answer and the
write. The cases are now: answered before the move (the check after the
compile), runs after the move, and throws after the move (the catch's).

The routing seed test asserted that nothing compiled the new namespace,
but its window covers the fork's first turn, whose compile is the turn's
own. It now records whether each compile's namespace held a message, and
pins that nothing compiled the fork while it held none, which is what an
emptiness check by compile would do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e cause

The re-seed's rule is that a pass a move has joined writes nothing and
disposes nothing, since the next pass judges every fork again on the
branch the store is on then. The seeding catch asked whether a move had
joined, but the branch beside it, for a template that's missing or whose
own re-initialization failed, disposed every empty fork without asking.
A template's refusal can be the move's own doing, so an undo joined to a
pass where the template refused could dispose a fork that the next pass,
with the template re-initialized, would have seeded. The check now sits
once before any disposal, for every cause.

A disposal for a failed template named "its alert" for the reason, but a
failure is reported from the last pass only, and a later pass can find
the template re-initialized, so that alert might never come. The cause
now carries the template's own error, and its remedy says "If the
template still refuses its work". A disposal for a template that isn't
registered names its remedy too: register an agent under that name, or
configure the conversation router with a registered template.

Each disposal is reported under the move whose branch its pass judged.
A move that joins later changed the verb the whole report used, so a
fork an undo's pass disposed was reported as the redo's. The pass now
stamps the verb it starts with on its disposals, and the alert's text
and its data's `after` use it.

A redo that drops its step and one beneath it says "with the one beneath
it, whose branch is gone too". The joined path's comment in
beginSyncMove says what it checks and why the reservation's other
checks still hold under it.

Tests: a template refusal in a pass an undo joins disposes nothing, and
the next pass seeds the fork the joined undo emptied (two of the
template's turns with a fork spawned between them; the second undo
joins once the fork's own re-initialization in the first pass has
finished, while the template's is held, which then refuses);
the joined-move disposal is reported under the undo, in its text and its
data; the template's error and the unregistered template's remedy are
pinned in their tests; a re-seeded fork's consumed mark covers the
seeded copies on the landed branch; and the two tests that an undo's
re-initialization waits for maintenance or an over-budget drain check
that the re-initialization has reached that wait (a promise that records
when it's awaited), rather than sleeping a fixed 20 ms, so neither can
pass without reaching it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Duncan-2410 and others added 7 commits October 21, 2026 15:33
… check for any stop of its lifecycle, and a restart's word for a conversation agent

The fixes from this round's third review (at 778a04d), as the PR's
author ruled them.

- A spawn's check refuses once a stop() of its lifecycle has begun: one
  under way or completed (stopPromise, claimed before the teardown's
  first steps, which can emit traces while the framework still runs), or
  one that failed with no start() since, which leaves the framework not
  running with its queue closed. stopPromise alone admitted a spawn after
  a failed stop() that left the store open, and after a completed stop()
  and an addModule(), both of which clear it. A start() sets running, so a
  spawn after a stop() and a start() still goes through.
- The re-initialization alerts' texts are built by pure functions
  (refusedWorkAlert, restoreAlert, disposalAlert), each giving its
  opening, what happened and what to do or what happens next, and the
  whole message. A restore's alert names the branches after its opening,
  so every opening's length is set by its shape: at most 338 characters,
  within the 343 to 349 the webhook keeps of a long message's start, by
  the message's length. A test
  checks every shape the builders make, with causes of 3,000 and 200,000
  characters and branch names of 200, against opsWebhookText, the cut
  opsAlert posts. REINIT_MOVES and TEMPLATE_REMEDIES type the moves and
  the template remedies, so a new one joins the test's shapes.
- A restart doesn't create a conversation agent again, since the
  router's bindings are in memory, so for one the remedy says a restart
  ends it and its channel's next qualifying message spawns a new fork,
  which gets none of the writes queued for it, and a failed restore's
  status says so too. It said a restart re-initializes it.
- A disposed fork's held writes: its window's release stores what its
  store takes of them, now or at resume on a quiesced host, and passes
  any failed reply on to the primary; a stop before that resume leaves
  them queued, and the restarted host drops them when it resumes, if its
  persisted queue kept them, passing any failed reply on too. The
  comments say the same.
- The opening says "a restart re-initializes it too, but loses them
  unless the deferral queue is persisted"; after it, that the refused
  work includes compiles and message writes, and that quiescing the host
  before it stops persists the deferral queue.
- After the disposal's opening, that the next fork is seeded from the
  template, under a new name.
- The fragment tells a restore that couldn't select the source after a
  suppression's removals failed, whose agent serves, apart from the others,
  whose agent refuses its work.

Tests:
- A spawn after a failed stop() and after a completed stop() and an
  addModule() is refused, one after a start() is made, and one that
  reaches its check in stop()'s first steps, while the framework still
  runs, is refused.
- Two tests adapted from a reader's probe: a fork disposed on a
  quiesced host, with two channel writes and a failed reply held for it,
  at the host's resume (its window takes them, and the primary is told
  of the failed reply), and at a stop before the resume and a boot on the
  same store (the restarted host drops the writes at its resume, and tells
  the primary).
- A suppression whose removals fail after its fork, and whose restore
  selects the source and then fails to re-initialize, says its agent
  refuses its work: selected wins over managerCurrent.
- A fork whose re-initialization fails after a branch switch, and a
  rollback of a fork whose restore fails too, say what a restart does to a
  conversation agent.
- The first long-hold test's title says it calls the hold's reporter.
- The webhook test of two alerts says which two it posts.
- The alert assertions follow the new texts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ove)

main moved by 14 pull requests since this branch's last merge of it
(ea4297a, in 180b703). git stops at four places in src/framework.ts,
each resolved by keeping both sides:

- After EphemeralRun, anima-research#608 adds scriptResultBrief and this branch adds
  the re-initialization alerts' builders and ManagerReinitFailure. Both
  are kept, the builders first.
- Where a channel message or a push is written into a conversation
  fork, anima-research#252 stamps the stored copy's digest (withStoredDigest) and this
  branch writes through writeToFork, which defers the write while a move
  holds the store. writeToFork now gets the stamped metadata, so a write
  it defers lands with the digest of the blocks it stores.
- readdDrained: anima-research#616 passes a failed reply queued for a target that's no
  longer registered to the primary, and this branch adds the traceAs
  option. Both are kept: the trace covers what the drain stored, the
  passed replies included.

main's other hunks within 40 lines of this branch's were read in the
merged file: anima-research#616's rework of stop()'s comment and its pass for
postponed releases, beside this branch's status lines for writes kept
queued, and anima-research#622's withoutHostKeys, beside writeToFork's call.

anima-research#616 changes what stop() does to a fork a quiesced host disposed: the
pass for its postponed release now drains what was queued for it, so a
failed reply goes to the primary, deferred again, and the rest is
dropped, logged, at the stop() rather than at the restarted host's
resume. At this merge the test "a fork disposed on a quiesced host: a
stop before the resume leaves its held writes queued, ..." fails for
that (0 entries queued, not 3), and the disposal alert's sentence and
the description's Not covered point still say what main did before
anima-research#616. The commit after this one adapts them.

Checked at this merge's tree before committing: tsc and the build pass;
npm test 6,010 pass, 1 fail (that test), 44 skipped of 6,055; the three
files CI runs alone 130 of 130.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s restart on the store's branch, and a disposed fork's held writes as its release went

The fixes from the round's fourth review (at 939c58e), as the PR's
author ruled them, built on the merge of main 97ca7eb (ed249de).

- What a restart does is said by the agent's kind (AgentKind): a
  configured agent, one of the config's agents or its subconscious, is
  created again by create(); a conversation agent isn't, since stop()
  ends each live fork (anima-research#616) and the router's bindings are in memory;
  and a run isn't, since stop() ends each run. A move's pass
  re-initializes every registered agent, a run included, so a run whose
  manager fails is told that a restart ends the run, where it was told a
  restart re-initializes it. The kind is taken when the manager fails
  (ManagerReinitFailure.kind, by agentKind, the framework's own test for an agent
  a restart doesn't create again, as holdProviderAcceleration's), since a
  fork can be disposed before the failure is reported. store-held-long
  said a restart re-initializes every manager; it now says a restart ends
  the conversation agents and runs and re-initializes the configured
  agents, whose queued writes outlast the process only in a persisted
  deferral queue.
- A failed restore that left the store on the failed branch said "a
  restart re-initializes it too". A manager works on the branch the store
  is on, so a restart would serve a configured agent from the branch it
  shouldn't run on. The opening says so, and the restart's loss of the
  queued writes moves past the opening, where it fits; the error's status
  names that branch.
- With anima-research#616: a disposal records whether its window's release was
  postponed (releasesAfterResume, after the disposal), and the alert says
  only what that makes true: a release that ran stored what its store took
  of the held writes in the fork's window and passed any failed reply on;
  a postponed one does so at the host's resume, and a stop() before the
  resume passes a failed reply on and drops the rest, logged, which anima-research#616
  made stop() do for any release a quiesce postponed. Before, the alert
  said a stop before the resume left them queued for the restarted host.
- With a reader's first point: a failed cleanup's sentence says "it"
  for one write, and that a flush meeting one still queued under the
  fork's name passes a failed reply on to the primary and drops anything
  else, logged.
- With a reader's second point: restoreAlert's managerCurrent is typed
  for a suppression only, the one move whose fork leaves a manager current
  on the failed branch.
- The fragment says the webhook limit holds for any alert without a key
  of its own, not for every kind.

Tests:
- The every-shape test adds a cause of a million characters, past which
  the webhook keeps 343 characters of the start, builds each refusing
  shape for each kind, and checks each one's restart clause and refusal
  detail.
- A run whose manager fails to re-initialize is told a restart ends the
  run, with the run leaving before the failure is reported.
- A suppression whose fork switches the store and fails, and whose
  restore can't select the source, raises an alert naming the suppression.
- The failed restore's alert and status, for a configured agent left on
  the failed branch.
- The early-spawn test starts its spawn from stop()'s first step,
  endConnectsInFlight, so a claim made after any first step fails it.
- With anima-research#616: the quiesced-disposal test's stop variant now finds, after
  the stop(), nothing queued for the fork, the two channel writes dropped
  with log lines, and the failed reply kept for the primary, which the
  restarted host tells at its resume; the channel writes are stored
  nowhere, in the primary's window or the fork's. Both variants check the
  alert's postponed-release text. The merge left this test failing; it
  passes now.

Measured with the built functions, at this tree but for the fragment: the
longest opening is 338 characters, a branch switch's disposal whose
cleanup failed, with the missing-template remedy; the failed restores left
on the failed branch are now shorter. tsc and the build pass, and the test
file passes 89 of 89.

The review's other points change nothing here: five didn't hold against
the code (one of them held at 939c58e, and anima-research#616 ended it), one wasn't
worth a change, and one names a gap already known.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… alert's names after its opening, and a disposal's release as it stands at the report

The fixes from the round's fifth review (at b254126, over the merge of
main 97ca7eb), as the PR's author ruled them.

- With a reader's point: what a restart does to an agent was read in a
  failure's catch, after the awaited re-initialization or restore, so a
  run its watchdog ended meanwhile had left ephemeralRuns and was taken
  as configured, and told a restart re-initializes it.
  reinitializeManagers now takes the kind before its await, and doesn't
  report a failure of an agent no longer registered when it fails, since
  nothing that's gone refuses anyone's work; restoreSourceBranch takes it
  before its await; the re-seed says 'conversation', since every agent it
  judges is one.
- store-held-long is built by longHoldAlert, a pure builder like the
  other alerts. Its opening says what the hold does and what a restart
  does to each kind of agent ("A restart ends the {move}, the
  conversation agents and the runs, ..."), at most 343 characters by its
  format, and the move's agent and what the hold is on come after it,
  since names have no bound: a conversation agent's runs to 97 characters
  (forkAgentName), and before, the restart warning came after two of them
  and the webhook's cut could drop it.
- Whether a disposed fork's window still waits for the resume is read
  when the report is made (releasesAfterResume), not recorded at the
  disposal: a disposal that threw after postponing its release didn't
  record it, and a resume before the report had already run the release.
  A postponed release gives the postponed text whether or not the cleanup
  failed; otherwise a failed cleanup gives its own text, since a cleanup
  that threw before the release released nothing; otherwise the release
  ran.
- A failed cleanup's held writes may be in the fork's window, still
  queued under its name, or in neither: releaseWindow takes them from the
  queue before it writes them.
- The fragment says a restart would serve a configured agent from the
  branch the store is on.
- A reader's cosmetic point: a line of readdDrained's doc comment the
  merge's splice left unwrapped.

Tests:
- A run that ends during its own re-initialization, which then fails,
  isn't reported; a run named by a rollback whose restore fails, and which
  ends during the restore, is told a restart ends the run, in the error's
  status and the alert.
- The every-shape test builds the long-hold alert for each move and each
  thing it can be on, with 200-character names and a manager name that
  grows with the cause's length, so its opening is checked against the
  webhook's shortest start.
- A fork disposed on a quiesced host whose cleanup then fails, after its
  release was postponed: its alert gives the postponed text and the
  cleanup failure, and the resume takes the writes held for it.
- The every-shape test pins the failed-branch restart wording to the
  restores left on the failed branch, and the other wording to the rest.
- The long-hold tests' expected texts follow the new order.

Measured with the built functions, at this tree before it was committed,
built from an empty dist: the longest openings are the long-hold alert's,
343 characters, for a branch switch, and the disposal's, 338. tsc and the
build pass, and the test file passes 92 of 92.

The review's other points change nothing here: two didn't hold against
the code, one is intended, and one wasn't worth a change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l and said as what it means, a restore's kind taken when its surgery reserves the store, and the long-hold opening

The fixes from the round's sixth review (at d99bf12), as the PR's
author ruled them, two of which reverse her ruling on the previous
review's points.

- Whether a disposed fork's window release was postponed was read from
  releasesAfterResume when the report was made, and "not in the map" was
  taken as "the release ran". stop()'s pass for postponed releases and a
  resume whose release throws empty the map too, and the stop drops the
  writes. It's recorded again at the disposal, now read right after
  disposeConversationAgent on either of its paths, so one that throws
  after postponing it still records it. The postponed text says what the
  postponement means, which holds whatever follows: "its window's release
  was postponed to the host's resume, which stores what its store takes
  of them there and passes any failed reply on to the primary agent; a
  stop() before the resume drops all but a failed reply, which goes to
  the primary at the restarted host's resume". It was in the future
  tense, and said a stop() passes a failed reply on, where on a quiesced
  host it's deferred again.
- reinitializeManagers took the kind before its await, which changed
  nothing: a failure is recorded only while this.agents still holds the
  agent, and every map agentKind reads changes together with this.agents,
  with no await between. It's read in the catch, after that guard, and
  the comments say so.
- A failed restore's kind was taken at restoreSourceBranch's start,
  after the surgery's own awaits, so a run that ended during the
  rollback's switch or the suppression's fork was named configured. Each
  surgery takes it when it reserves the store and passes it on.
- The re-seed checks the fork's registration after its awaited seed
  too, so a fork that left meanwhile, as one stop() ends, is neither
  reported nor disposed again. Its kind, the literal 'conversation', now
  says why.
- The long-hold opening: "The {move} has held the store for N s, so no
  turn starts and the writes it defers wait in the deferral queue (N
  writes now). A restart ends the {move}, the conversation agents and the
  runs, and re-initializes the configured agents; writes still queued at
  exit survive only in a persisted deferral queue." A clean stop() stores
  what it can, so only what's still queued at exit needs the persisted
  queue; the count says what it counts, "1 write" for one; and the opening
  is at most 333 characters with counts of nine digits, where the old one
  passed 343 at six. Its comment says a conversation agent's name runs to
  about 100 characters.
- The fragment says a failed re-initialization is reported unless the
  agent has left by then.

Tests:
- A run named by a rollback whose restore fails, and which ends during
  the rollback's switch or during the restore, leaving both this.agents
  and ephemeralRuns as a run's end does, is told a restart ends the run,
  in the status and the alert.
- The run that ends during its own re-initialization asserts that its
  initialize ran.
- A fork that leaves while it's seeded again, whose seeding then fails,
  isn't reported.
- The every-shape test asserts the long-hold restart sentence for each
  move, grows every long-hold row's agent name with the cause, and uses
  nine-digit counts.
- The quiesced test whose cleanup fails after the release was postponed
  says it stands in for any later step that throws: by reading none does
  today.
- The long-hold and postponed-release tests' expected texts follow the
  new wording.

Measured with the built functions, at this tree before it was committed,
built from an empty dist: the longest opening is the disposal's, 338
characters; tsc and the build pass, and the test file passes 94 of 94.

The review's other points change nothing here: one didn't hold against
the code, one is intended, and one wasn't worth a change (its test
stays, as above).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ext, and the round's tests

The fixes from the round's seventh review (at 0fa6d89), as the PR's
author ruled them, and a harness the round before broke.

- With a reader's point: the postponement lives in memory, so a crash
  while the host is quiesced lands the held writes at the restarted
  host's resume flush, which drops all but a failed reply as a stop()
  does. The postponed text says "a stop() or a crash before the resume
  drops all but a failed reply, logged, and that goes to the primary at
  the restarted host's resume".
- The fragment's long-hold line says writes still queued at exit survive
  only in a persisted deferral queue, and its disposal line says a stop()
  or a crash before the resume.
- test/discord-awareness-outbox.test.ts: since 0fa6d89 each surgery
  calls agentKind when it reserves the store, and that file's
  hostCommandFramework stub had neither conversationAgentHomes nor
  ephemeralRuns, so its two message-granular undo tests failed there (a
  full run at that head). A framework always has both; the stub now does.

Tests:
- A fork disposed on a quiesced host whose postponed release runs before
  the alert is made, as a resume between the two would: the alert still
  says the release was postponed, as the disposal met it.
- A run named by a suppression whose fork fails, and which ends during
  the fork, leaving both maps, is told a restart ends the run when the
  restore fails too.
- The run whose failure is reported after it leaves now leaves both
  this.agents and ephemeralRuns, as a run's end does.
- The fork that leaves while it's seeded again is disposed once.
- The every-shape test pins the long-hold count's wording, "N writes"
  and "1 write".
- Each rollback variant asserts its run ended where it says.
- The every-shape test's comments say 343 characters at a million, and
  that it checks the opening.

Measured with the built functions, at this tree before it was committed,
built from an empty dist: the longest opening is the disposal's, 338
characters; tsc and the build pass, and the round's test file with the
awareness outbox's passes 160 of 160.

The review's other points change nothing here: one is intended, one
didn't hold against the code, and three weren't worth a change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e restarted host's resume, and a test runs the crash

A reader's read of ece892e, as the PR's author ruled it: "a stop()
or a crash before the resume drops all but a failed reply" claimed what
no test ran for a crash, and put the drop before the resume, where a
crash drops nothing. While the host is quiesced, each change to the
queue writes its record (persistDeferredWrites), so after a crash the
fork's writes are on disk and the postponement is gone with the process:
the restarted host's resume flush passes the failed reply on and drops
the rest, logged (readdDrained's pass for targets no longer registered).

- The postponed text says "A stop() before the resume drops all but a
  failed reply, logged, and after a crash the restarted host's resume
  drops them the same way; that reply goes to the primary at the
  restarted host's resume." It follows the alert's opening, whose bound
  doesn't move.
- The fragment says the same.

Tests:
- A fifth variant of the quiesced-disposal tests copies the host's
  directory as it is on disk right after the disposal, which is what a
  crash leaves, stops the first host, and boots a second from the copy:
  its queue holds the three writes, and at its resume nothing stays
  queued for the fork, the two channel writes are dropped with a log
  line each and stored in neither window, and the primary is told of
  the failed reply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru

nissa-seru commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor Author

Replying to this review.

@Anarchid The round for your third review is at fcb94144. It merges main at 97ca7eba, and your review read 7dd0f878, so we're asking you to re-read it there before we merge it. Point by point, with the commit that made each change and the tests that pin it (in test/reinit-after-branch-move.test.ts unless named). The commits are 180b703d and ed249de5 (merges of main), fedc015b (R3-1), 82493341 (R3-2 to R3-6), 61662246, 778a04d0, 939c58ed, b254126c, d99bf120, 0fa6d89f and ece892ed (what our own seven reviews of that round found), 35785770 (a reader's point on the last of them), and fcb94144 (a test's timing check that CI's Node 20 run on Ubuntu caught).

  • R3-1. 180b703d merges main at ea4297ab; git merged it with no conflicts. ed249de5 merges main at 97ca7eba, keeping both sides at the four places git stopped, as the description's Merge notes say. In fedc015b, undoLastTurn()'s and redo()'s results carry settled?: Promise<void> when the store moved.
    • It settles once the store's reservation is released, every agent's context manager re-initialized or its failure alerted, and it never rejects, a release that throws included. An undo or redo made while an earlier one's managers re-initialize gets the same promise.
    • The API server's undo and redo await it before they broadcast branch:switched and answer. Their answers leave it out, so the JSON they send is as before.
    • The host/command undo, and these tests, await it where they read the private field before. main's test in test/mcpl-coalescing.test.ts that writes right after an undo awaits it first, and passes.
    • Tests: "the API server's undo and redo tell clients of the move once every manager is current, so a client that reads an agent's context then gets it", and "an undo whose release throws still settles its settled, and says the release failed".
  • R3-2. removePlacement's comment, the description and the changelog now say what this PR makes true: a coalesced removal leaves a stored message alone while the store is held. A module's edit or removal of a stored message, and a write carrying a fresh tool result, still reach a context manager during a hold, as on main, and the description's Not covered names them (82493341).
  • R3-3. The Not covered line is corrected. A write held for a conversation agent that the pass disposes goes with it: its window's release stores what its store takes there, where no agent reads it, and passes a failed reply on to the primary. On a quiesced host the release is postponed to the resume. A stop() before the resume drops all but a failed reply, logged, as main does since #​616, and after a crash the restarted host's resume drops them the same way; that reply goes to the primary at the restarted host's resume. The disposal alert says how many went, and which of these the disposal met, and when the disposal's cleanup fails, that they may be in its window, still queued under its name, or in neither (82493341, 61662246, 778a04d0, 939c58ed, b254126c, d99bf120, 0fa6d89f, ece892ed, 35785770). Tests: "a fork disposed after a move takes the writes held for it into its window, and its alert says how many", "a fork whose disposal fails partway after its seeding fails is reported with both, and the pass seeds the next fork", now with a write held for it, and the "a fork disposed on a quiesced host…" tests, which take two channel writes and a failed reply through the host's resume; through a stop(), which drops the channel writes, stored nowhere, and keeps the failed reply for the primary, and the restarted host's resume; through a crash, with a second host booted from a copy of the first one's directory taken right after the disposal; through a cleanup that fails after the release was postponed; and through a release that runs before the alert is made.
  • R3-4. A move whose hold on the store is still up after 60 s raises one store-held-long ops alert (82493341, 61662246, d99bf120, 0fa6d89f).
    • It opens with how long the move has held the store, that no turn starts meanwhile, how many writes wait in the deferral queue, and what a restart does: it ends the move, the conversation agents and the runs, and re-initializes the configured agents, and writes still queued at exit survive only in a persisted deferral queue. Then it names the agent the move was for, and the manager a re-initialization pass is on at that moment, or the maintenance the move is waiting for.
    • Its timer is armed when the store is reserved and cleared at its release, so it covers every step that holds the store, for every move that holds it: an undo or redo, and the rollback, suppression and branch switch that main already holds the store for.
    • The hold isn't shown in the host's status or health, and the description's Not covered says so.
    • Tests: "a hold's reporter, called during the pass, names the move, its agent, the manager the pass is on and the writes waiting", "a rollback still waiting for maintenance that was running when it took the store raises the alert, saying that's what it waits on", "a branch switch still holding the store for its config mount after its pass names no manager", "a suppression's hold is named as the suppression", "between two managers' re-initializations, the hold names neither" and "a hold let go before its threshold says nothing after it".
  • R3-5. context-reinit-failed's remedy, and a failed restore's, now say that a switchBranch() recovery stores the agent's queued writes, at resume on a quiesced host, and that a restart re-initializes it too but loses them unless the deferral queue is persisted. For a conversation agent, which a restart doesn't create again, they say a restart ends it and its channel's next qualifying message spawns a new fork, and for a run, which stop() ends, that a restart ends the run. A failed restore that left the store on the failed branch says a restart would re-initialize the agent there, on the branch it shouldn't run on (82493341, 61662246, 778a04d0, 939c58ed, b254126c, d99bf120, 0fa6d89f).
    • stop() now says, once per agent, how many writes its last flush leaves queued because the agent's store didn't take them, and whether the queue keeps them for boot. That line, and main's line for writes left queued behind a stream still running, say the persisted queue's record is behind, rather than that boot keeps them, when its last write failed or met the cap.
    • Tests: "the alert's remedy says a restart loses the writes kept queued, and stop() says how many it leaves for an agent whose manager refuses them", the failed-restore tests, and each line's three wordings, in this file and in test/stop-with-work-in-flight.test.ts.
  • R3-6. The description and the changelog scope "without a restart" to the context managers. The description's Not covered says the channel lifecycle and tune-out state stay as they were on the branch the store left until a restart, as on main.

Beyond your points, from our own reviews of the round and a reader's (61662246, 778a04d0, 939c58ed, b254126c, d99bf120, 0fa6d89f, ece892ed, 35785770):

  • Each re-initialization alert opens with what happened and what to do, short enough to stay within the start the ops webhook keeps of a long message, whatever the branches it names, and gives its cause last. The texts are built by refusedWorkAlert, restoreAlert, disposalAlert and longHoldAlert, and "is in what the ops webhook keeps of the message, in every shape the builders make" checks every shape against the webhook's cut, with long causes, long branch and agent names, and counts of up to nine digits.
  • A suppression whose removals fail after its fork, and whose restore can't select the source, now says its agent serves from the failed branch until a switchBranch() moves it, rather than that it refuses its work: the fork had initialized its manager there. Test: "a suppression whose removals fail after its fork, and whose restore can't select the source, says the agent serves from the failed branch".
  • A spawn that comes to create a conversation agent once a stop() of its lifecycle has begun, under way, done, or failed with no start() since, as one waiting out a hold can, fails as a failed spawn does, rather than opening its context manager on a store stop() is tearing down or has torn down. Tests: "a spawn that waits out a move's hold into a stop() fails, as a failed spawn does", "a spawn after a failed stop() is refused, and so is one after a completed stop() and an addModule(): neither begins a lifecycle", "a spawn that reaches its check in stop()'s first steps, while the framework still runs, is refused", and "a spawn after a stop() and a start() is made: the check is for a stop() of this lifecycle".
  • Which kind an agent is decides what each alert says a restart does to it: a configured agent is created again, and a conversation agent or a run isn't. The kind is read while the agent is still registered, and a rollback or suppression takes it when it reserves the store, so a run that ends during the move keeps its words. A failure of an agent that has left by the time its re-initialization or seeding fails, as a run its watchdog ended, isn't reported, since it refuses no one's work. Tests: "a run whose manager fails to re-initialize is told that a restart ends the run", "a run that ends during its own re-initialization, which then fails, isn't reported", the rollback and suppression tests whose run ends during the move, and "a fork that leaves while it's seeded again, whose seeding then fails, isn't reported".
  • The re-seed no longer skips the agent a rollback or suppression names: a suppression refuses to remove an agent's last message and a rollback keeps its tail, so that agent is never empty there, and the skip never changed an outcome.

The description's Evidence section has this round's runs.

Written by Duncan-2410.

— Daniel-2612

… millisecond

The test of a rollback still waiting for maintenance asserted that the
store-held-long alert reports a hold of at least its 1,000 ms threshold.
The hold is measured by Date.now() from the reservation, and the timer
runs on libuv's clock, each to the whole millisecond, so the alert can
read just under its threshold: CI's Node 20 run on Ubuntu read 999. The
assertion now allows 5 ms, as tool-result-guard's duration check does,
which still tells an alert at its threshold from one that comes early.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Duncan-2410 and others added 8 commits October 25, 2026 23:09
…ove)

main moved by 605 commits since this branch's last merge of it
(97ca7eb, in ed249de). git stops at five places in src/framework.ts and
one in test/mcpl-coalescing.test.ts:

- Where a channel message, or a correction delivered to a fork, is
  written into a conversation fork, main stores the clock row due ahead
  of it (storeDueClock) when the fork has no turn alive, and this branch
  writes the message through writeToFork, which defers it while a move
  holds the store. Both are kept, and the clock is stored only when no
  move holds the store: a deferred message lands at the hold's release,
  and a clock written now would land in the held namespace ahead of the
  move's re-initialization and re-seed, apart from the message it
  precedes. The next message after the hold gets the clock, as a clock
  that can't be stored is skipped (storeDueClock). main's
  router.touch(channel) is kept.
- createConversationAgent: main's signature, a ChannelRef home, with this
  branch's wait for a move's hold and its check for a stop that has
  begun.
- The fork's seeding: this branch's seedConversationAgentIfEmpty, with
  main's changes to the inline seeding moved into it: the template's
  stored context injections left out by the compile's provenance, its
  channel and by-name notices left out (withoutPrimaryNotices), and
  logSeedOmissions' new arguments, and main's wording of the compile's
  thinking binding and the notices paragraph in its comment.
- readdDrained: main's account of what passes to the primary for a
  target that's gone, with this branch's traceAs option.
- test/mcpl-coalescing.test.ts: main's wording of the answered
  watermark, with this branch's await of the undo's settled.

test/reinit-after-branch-move.test.ts gives the router and
createConversationAgent a ChannelRef ({ serverId: 'world', channelId })
where it gave a channel id, the router's API since main's change.

Checked at this merge's tree before committing: tsc and the build pass
from an empty dist, and the six files the merge touches
(reinit-after-branch-move, discord-awareness-outbox, mcpl-coalescing,
stop-with-work-in-flight, stop-with-host-operations, conversation-routing)
pass 490 of 490.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s and a platform constraint

A comment stays only when it's a license header, a note on non-obvious
behaviour forced by an external dependency, platform, vendor or protocol,
a directive, a doc comment defining a public API contract, or an issue or
RFC link explaining a constraint the code can't express. The other
comments this branch added against main go: their reasons are in the
commit messages that added them and in the pull request's description.

What stays of this branch's comments:
- the public docs of switchBranch(), undoLastTurn() and redo(), which say
  what a caller meets: the store's reservation until every context manager
  is re-initialized, the context-reinit-failed alert, and the result's
  settled;
- in the rollback long-hold test, the note that the hold is measured by
  Date.now() while the timer fires on libuv's clock, each to the whole
  millisecond (Node's own behaviour).

Where this branch had rewritten one of main's comments, main's wording is
back, except where this branch's change made it false: the seed's comment,
moved with the seeding into seedConversationAgentIfEmpty, says the
template's compile is the one dry run, since the fork's emptiness is no
longer a compile. The test of two API switches handled together loses
main's comment, which described the behaviour that test no longer pins.

Only comment lines change: every line this commit removes or adds is a
comment line, but for one trailing comment taken off a test's await.
Checked before committing: tsc and the build pass from an empty dist, and
the six files this branch's tests are in pass 490 of 490.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…at reaches the primary

main now passes on to the primary, for an agent that has ended, a channel
notice that carries its inputs and a module's write for a run or a fork,
beside a failed reply (passesToPrimary): the window's release, stop()'s
pass for postponed releases and a flush of the queue all use it. The
disposal alert's three held-writes sentences, and the changelog fragment,
still said only "a failed reply". Each now names the three kinds: a
failed reply, a channel notice the fork could have acted on, or a module
write for it.

The quiesced-disposal tests hold a channel notice and a module's write
beside the failed reply, and check that each reaches the primary at the
resume, at the restarted host's resume after a stop() before the resume,
and at the restarted host's resume after a crash. The channel the notice
says opened is decided open in the store's channel lifecycle log, which a
host with no channel registry reads it from, and the store is synced
before the crash copy, as a host's sync interval would have long before a
crash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nel decision still stands

The disposal alert's held-writes sentences and the changelog fragment
said a channel notice the fork could have acted on goes to the primary.
main passes one on only while the channel's durable decision still
stands: open or tuned out for a channel-opened notice, still decided open
and not open for a failed open. Otherwise it drops the notice, logged
(channelNoticeForPrimary). The sentences now say "a channel notice whose
channel decision still stands".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…agent one remedy, and narrow the withdrawn tick

Three parts of this change are cut back, and one is narrowed.

A conversation agent with no stored message of its own on the branch the
store moves to is now disposed, its channel unbound and an alert raised,
in place of seeding it again from the template. Per-channel conversation
routing is deprecated, and the re-seed carried a template-health path,
a check for a store moved mid-seed and a callback threaded through every
pass. The disposal runs once, after every manager is re-initialized for
the branch the store lands on, with no await inside it: an undo and a redo
made before the pass ends judge the branch the redo lands on, so a fork
the redo restores is kept. A fork is disposed whether or not its own
re-initialization succeeded, and only the disposal's alert is raised for
it: its message count reads the store's branch directly, and the
refused-work alert's remedy, a switch to the current branch, would dispose
it rather than recover it. The cost: an undo and then a redo don't bring a
disposed fork's conversation back; the channel's next qualifying message
spawns a fresh fork from the template, which needs the template's own
context manager to be working.

The disposal alert gives the number of writes held for the fork while the
store was held, and no account of where they go. That is the window
release's behaviour, which this change doesn't alter. A run showed that a
module's write held for a disposed fork passes to the primary, and a fork
seeded from the template, when the template is the primary, carries it.

Every agent kind gets one remedy: a switch to the current branch
re-initializes it and stores its queued writes, at resume on a quiesced
host; a restart can lose them. That makes no claim about what a restart
does to the agent, so the agent-kind threading goes. The refused-work and
restore alerts no longer say that a restart ends a conversation agent or a
run; the long-hold alert still says what a restart does, as it needs no
agent's kind.

Creating a conversation agent goes back to judging its namespace empty by
a dry-run compile of it, as before this change; the stored-message count
was only shared with the re-seed.

A maintenance tick an undo or redo moved the store under is withdrawn only
when it meets context-manager's refusal of work on a branch it isn't loaded
for, "requires reinitialization for the current branch generation" or
"refused stale branch-scoped mutation". Any other failure it meets, in the
tick or in the readiness read after it, keeps its maintenance-failure
alert.

The tests follow: those for the re-seed and the per-kind texts go, the
disposal's are added, the alert texts are checked as facts rather than
whole sentences, setups go through public routes where one helper allows
(a real turn records the checkpoint, a module writes for an agent, a held
stream makes an agent busy, a held maintenance tick is the running
maintenance), and a coalesced update or retraction made while a branch
switch holds the store is checked where a caller meets it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a disposed fork's alert gives its failed re-initialization

A spawn stored its channel's name counter before it waited out a move's
hold. During a rollback's or a suppression's wait for maintenance already
running, the store hasn't moved yet, so the counter landed on the branch
the store was about to leave, while the fork was made on the branch it
landed on. After a restart, the channel's next fork took that fork's name,
over its namespace. The spawn now waits out the hold first, with the same
one-line wait as creation's; a helper awaited there would yield a
microtask after its last check, in which a move could take the store.

A conversation agent disposed after a move, whose own re-initialization
had failed, had that failure's cause reported nowhere. The disposal alert
now gives it, before a failed cleanup's.

The disposal alert said the channel's next qualifying message spawns a
new fork, which is false while the template's own context manager
refuses its work, since a spawn compiles the template. It now says the
spawn needs that manager to be working.

A suppression no longer judges the conversation agents for disposal: it
forks the store at its head, keeps every other agent's messages, and
refuses to remove its own agent's last one, so it can't empty one.

The log line for a failure in an undo's or redo's pass now names the
re-initialization or the release, where it named the release alone.

The merge of main 0a4a2c6 had added a check of the store's hold to
main's condition for a clock row ahead of a conversation agent's channel
message and correction. A move's hold already holds every agent's turn,
and the row is stored only while the agent has no turn alive, so the
check decided nothing. It's gone, and main's condition stands.

Tests: disposal after a branch switch, a rollback and a redo back from an
undo's branch; a clock-keeping fork emptied by an undo and re-initialized
while the pass still holds the store, with a channel message and a
correction for it then, still disposed; a fork spawned during a rollback's
wait keeping its name to itself after a restart; the next fork after a
disposal reached through the framework's own routing; a branch switch
waiting for a running maintenance tick; the long-hold alerts held past
their threshold to show there is one, and their restart facts in the shape
test; a disposal whose re-initialization failed, with a long cause,
keeping its opening in what the webhook posts; the event loop checked to
turn between the managers' re-initializations; and exact counts and
stop-line facts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ops webhook posts at most once per agent and kind in 15 minutes. A
conversation agent's refused-work alert and its disposal alert were both
context-reinit-failed with no key, so a refusal posted for a fork within
that time silenced the fork's later disposal, and the last post an
operator saw said a switch to the current branch would recover a fork
that was gone. The disposal alert now has its own key; a fork's name
isn't reused, so that costs at most one more post per fork. The changelog
fragment says so.

Tests: a fork that refused its work and is then disposed has both alerts
posted; a rollback asked for in the same step as a mention that spawns a
fork is refused as busy, with OperatorActionError (agent-busy); the disposal alert says the next spawn needs the
template's own context manager to be working; a disposal whose
re-initialization and cleanup both failed carries both causes; the
clock-keeping fork test checks each deferral is placed for that fork, and
opens its gate in a finally.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
0.27.0's undoLastTurn() and redo() promised an atomic roll-back and a
result, with no refusal. By our reading of its code, a runtime-settings
change went ahead while a move held the store, and a second switchBranch()
was refused only while the first still waited for running maintenance or a
workspace module's mount; a run shows two switches made together, with
neither, both going ahead. This change
refuses each of them with OperatorActionError (agent-busy): an undo or redo
while another agent sharing the store is busy or a conversation agent is
being created; a switchBranch() while another move holds the store for its
re-initialization, two made together included; and a runtime-settings
change, reset or cancelled transition while a move or a surgery holds the
store. Those refusals move from the changed fragment into a breaking one,
which names who needs to act and how to retry. The changed fragment keeps
the rest: what waits for the release, coalesced updates landing after it,
and the named agent's own busy refusal becoming an OperatorActionError
where it was a plain Error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru

Copy link
Copy Markdown
Contributor Author

Replying to this review.

@Anarchid #​353 is at f5dba8f6. Since our reply at fcb94144, we took the first option you gave for R1-1: a conversation agent a move leaves with no history of its own is now disposed rather than seeded again. That made R3-3's and R3-5's texts shorter. The remaining parts of R3-2 and R3-6 are filed as #618 and #619. The commits since fcb94144 are e2e2b8ce (a merge of main at 0a4a2c69), 5c293be9, 047a1033, f96b8d91, 77b79fc7, 09052cb8, 1269c003 and f5dba8f6.

  • R1-1. In 77b79fc7, after a rollback, a branch switch, an undo or a redo, a conversation agent with no stored message of its own on the branch the store lands on is disposed and its channel unbound, and an ops alert says so. Its channel's next qualifying message spawns a fresh fork from the template under a new name. The re-seed is gone, and so are its template-health path, its check for a store moved mid-seed, and the callback that ran through every pass.
    • When it's judged: once, after every manager is re-initialized for the branch the store lands on, with nothing awaited while it's judged. A fork that an undo empties and a redo made meanwhile restores is kept. A suppression can't empty an agent, since it forks the store at its head and refuses to remove an agent's last message, so it disposes none.
    • The cost: an undo and then a redo don't bring a disposed fork's conversation back. The description says so.
    • The ephemeral-candidate half is still An ephemeral agent created before the store's branch moves runs without its task afterwards, or refuses #383.
    • Tests: "an undo to before a fork was spawned disposes it, unbinds its channel and says so, and the channel's next mention spawns a fresh fork; a fork spawned before that point stays", "a branch switch that leaves a fork with no stored message of its own disposes it and unbinds its channel" with its rollback and redo variants, and "an undo that empties a fork, and a redo that restores it before the pass ends, leave the fork bound and say nothing".
  • R1-3. A maintenance tick that an undo or redo moved the store under is withdrawn only when it meets context-manager's refusal of work on a branch it isn't loaded for. Any other failure it meets, in the tick or in the readiness read after it, keeps its context-maintenance-failed alert (77b79fc7). Tests: the tick variants with each refusal text, and with another failure in the tick and in the readiness read.
  • R3-2. The paths that remain, a module's edit or removal and a write carrying a fresh tool result during a hold, are #​618. The description's Not covered names them.
  • R3-3. The disposal alert gives the number of writes held for the fork while the store was held, and no account of where they go (77b79fc7). Where they go is main's window release, which this change doesn't alter. Since fix(stop): end each live conversation fork as its release would #616, a failed reply, a channel notice whose channel decision still stands, or a module's write goes to the primary (047a1033, f96b8d91 named those three), and the rest is stored where no agent reads it. A run showed that a module's write held for a disposed fork reaches the primary, and the fork the channel spawned next from the template, which was the primary, carried it. So an account in the alert would have had to follow every case, and the count is what holds in each. Since 09052cb8, a disposed fork whose own re-initialization failed has that failure's cause in its disposal alert.
  • R3-4. store-held-long is as in our last reply. The hold isn't added to the host's status or health: the alert reaches the operator when a hold matters, and a status or health field would be a new field in the public API with no consumer yet.
  • R3-5. Every agent's alert now gives one remedy: a switchBranch() to the current branch re-initializes it and stores its queued writes, at resume on a quiesced host, and a restart can lose them (77b79fc7). The alerts no longer say what a restart does to the agent itself, which differs by kind, and the clauses for each kind are gone. store-held-long still says what a restart does. stop()'s lines for queued writes are as in our last reply.
  • R3-6. #​619.

Also since our last reply:

  • The merge of main at 0a4a2c69 (e2e2b8ce). The description's Merge notes say how each conflict was resolved.
  • 5c293be9 takes out the comments this branch added, except public API docs and one comment on a platform constraint, under the comment practice we now follow in every repository we work in. Where the branch had rewritten one of main's comments, main's wording is back unless the change made it false.
  • A spawn now waits out a move's hold before it stores its channel's name counter (09052cb8). Before, a spawn during a rollback's or suppression's wait for running maintenance stored it on the branch the store was leaving, so after a restart the channel's next fork took that fork's name, over its namespace. Test: "a fork spawned while a rollback waits for running maintenance keeps its name to itself after a restart".
  • A disposal's alert has a webhook key of its own (1269c003), so a refused-work alert posted for the same fork within the webhook's 15-minute cooldown doesn't silence it.
  • The changelog now files the new refusals as breaking (f5dba8f6): an undo or redo while another agent sharing the store is busy or a conversation agent is being created; a switchBranch() while another move holds the store for its re-initialization, two made together included; and a runtime-settings change, reset or cancelled transition while a move or a surgery holds the store are refused with OperatorActionError (agent-busy). By our reading of 0.27.0's code each went ahead there, except a second switch while the first still waited for running maintenance or a workspace module's mount; a run shows two switches made together, with neither, both going ahead on 0.27.0.

The description's Evidence section has this head's runs.

Written by Duncan-2410 (Claude Opus 5.5).

— Daniel-2612

One conflict, in src/framework.ts's createConversationAgent: this branch
marks a conversation agent's spawn as in flight while it waits out a
store hold, and main now opens each conversation agent's context manager
under a release record, so the framework can withdraw its calls once it
is done with the manager. Both lines stay, the spawn mark first; the
merged function closes a failed creation's manager and forgets its name
(main's catch) and clears the spawn mark (this branch's finally).

A fork this branch disposes after a branch move goes through
disposeConversationAgent, which on main now releases its context
manager, so a disposed fork's calls are withdrawn as any other ended
conversation agent's are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru
nissa-seru merged commit 0f1fd7b into anima-research:main Oct 26, 2026
12 checks passed
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 26, 2026
main brought anima-research#624 (a nested mount's directory compared as disk compares
names), anima-research#353 and anima-research#366. Where anima-research#624 met this change:

- effects.ts and observe.ts: main's judgement of a nested mount's place
  (holder.names, judgedByInclude, an awaited nestedAtReal), with this
  change's LEAVES_MOUNT for a link that leaves the mount.
- index.ts: main's imports from observe.ts with this change's DIRECTORY,
  LEAVES_MOUNT, NOT_FOLLOWED and NOT_REGULAR. NestedMountError keeps main's
  constructor and its "may lie in" words, and stays a Refusal, so the
  tools lead it as they do every refusal.
- test/workspace-path-spelling.test.ts: anima-research#624's tests that pin a tool's
  refusal now pin it led ("The read did nothing: ...", "The write did
  nothing: ..."), a link's reason as a sentence, and a followed link out of
  its mount in readRefusal's words; main's refusedAsInner takes the place
  of two of this change's literal assertions. The tests that run only where
  disk folds case, or folds a sharp s with ss, were edited by reading.

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 26, 2026
main brought anima-research#813, anima-research#697, anima-research#366, anima-research#353 and anima-research#624 (a nested mount's directory
compared as disk compares names). Where anima-research#624 met this change:

- effects.ts and observe.ts: main's judgement of a nested mount's place
  (holder.names, judgedByInclude, an awaited nestedAtReal), with this
  change's LEAVES_MOUNT for a link that leaves the mount.
- index.ts: main's imports from observe.ts with this change's DIRECTORY,
  LEAVES_MOUNT, NOT_FOLLOWED and NOT_REGULAR. NestedMountError keeps main's
  constructor and its "may lie in" words, and stays a Refusal, so the
  tools lead it as they do every refusal.
- test/workspace-path-spelling.test.ts: anima-research#624's tests that pin a tool's
  refusal now pin it led ("The read did nothing: ...", "The write did
  nothing: ..."), a link's reason as a sentence, and a followed link out of
  its mount in readRefusal's words; main's refusedAsInner takes the place
  of two of this change's literal assertions. The tests that run only where
  disk folds case, or folds a sharp s with ss, were edited by reading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tel-0s added a commit that referenced this pull request Oct 27, 2026
268 commits from main, 28 pull requests: #353 (every agent on the store
re-initialised after a rollback, suppression, switch, undo or redo),
#817, #835, #818, #820, #815, #825, #821, #824 and the rest, with main's
manager-release machinery (an ephemeral run's auxiliary membrane, a
tracked manager released with its agent) and writeToFork (a fork's
channel message held behind a live surgery). Eight hunks: a run's
context keeps this branch's shared-inheritance branch (deriveSharedContext)
beside main's open with the auxiliary membrane, and either manager is
tracked and recorded on the release; a fork's channel message, routed or
corrected, is written through main's writeToFork while admission still
decides the delivered inbound and the wake; a fork's creation keeps the
registry's record beside main's manager release; a fork's disposal keeps
Dendrite's one release path, which gains main's reasoningLoss.forget and
releaseManager (and the name-only branch its forget). .gitignore and the
diagnostics test take main's words. #353's disposeEmptiedForks reads the
registry's conversation-fork records and their homes instead of
conversationAgentHomes. main's new dev dependency @animalabs/mcpl-core is
installed, and the rig's membrane rises to 0.7.2 (connectome-host main
asks for it).

Eight rows of main's new tests are read onto this branch, each named in
a comment: discord-awareness-outbox's fake frameworks carry the registry
stub an undo's fork sweep reads; turn-continuation's fork check reads the
registry's record; reinit-after-branch-move's fork tests match a fork's
stored channel message by its author-prefixed form and allow the
derivation notice a spawn tells its source; subconscious-model-fields
configures the persistent reader, the one that is an agent at create;
workspace-lost-contents' five torn-blob cases probe whether the linked
chronicle rewrites a blob whose bytes differ (chronicle main does, from
the #60 series; the released 0.4.0 keeps the file) and assert each
behaviour. Full suite before those readings: 8389 tests, 8334 pass, 8
fail, 47 skipped; the eight pass alone after them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
tel-0s added a commit that referenced this pull request Oct 27, 2026
…catch-up

main's 0.27.0 and the 28 pull requests after it, by way of feat/dendrite's
ninth catch-up, meet the presentation index. Thirteen files conflicted:
src/framework.ts (51 hunks), src/agent.ts, src/gate/event-gate.ts,
src/modules/history/index.ts, src/tool-result-guard.ts, src/types/agent.ts,
six test files and one changelog fragment.

The branch move. #353's re-initialization (reinitializeManagers: every
manager's strategy set again, one at a time, its failures alerted, emptied
forks disposed) is now the mechanism of every move, and the fix wave's
resync (cd93606) is rebuilt on it: followBranchMove calls
reinitializeManagers, then rebases each agent's window on the new branch
(written down where the branch has none) and drops every redo entry, and
returns the failures for main's reportReinitFailures. What of cd93606
duplicated #353 is removed: its own reinitializeOnStoreBranch pass with its
retries, create()'s refusal of a context-manager without that method, and
three tests of the co-resident's strategy after a rollback, a suppression
and a failed restore, which #353's tests already make. switchBranch keeps
the fix wave's checkout logging around main's reinitializing/report shape.
A rollback follows the move right after its switch, so the agent's window
is presented on the new branch before holdUntilStart holds the rows the
cut returned; a suppression follows at its release, after the removals, as
main has it; restoreSourceBranch reports main's restore alert and leaves
the following to its caller.

Undo and redo stay on the window (decision 4): they move no branch, so
main's beginSyncMove and reinitializeAfterSyncMove, their syncMoveReinit
state and the maintenance and drain-kick arms that excused a tick an undo
moved the store under are not carried; an undo re-initializes nothing,
retracts, and every agent goes on compiling. The result types keep main's
optional settled, never set here, and the host /undo no longer waits on it.
This is either/or: main's undo reserves the store; this branch's undo
reserves nothing.

The queue's absence. stop()'s drain of the deferred queue (main reworded
its lines for a persisted record that is behind) and the release's flush
(main gave it a trace name) stay deleted. reportLongHold and
disposeEmptiedForks count the writes the surgery's hold keeps
(surgeryHeldWrites); the long-hold alert says those wait in memory and a
restart loses them. A manager that refuses its work after a failed
re-initialization keeps no queued writes here: a write for it fails at its
writer, or at the hold's release, and is logged, and the alert's remedy
says so (main keeps them queued until a switch recovers it: either/or).
The release now traces each write it stores, with its id (source
surgery-release), as main's flush did. writeToFork (decision 3) is rebuilt
on storeInFork: a fork's channel message or correction is stored at once
and presented at the fork's next point, and under a surgery's hold it is
kept in memory and replayed at the release, its placement naming the fork
with no id yet. The idle closure prompt stays the fork's own row
(storeOwnRow with present), the sweep waiting out a hold.

Wakes. main's readsStoredMessage (#814, a wake for a message stored as it
arrived) is folded into readsRow, which names a stored row already:
readsRowFor gives the stored id whether or not the row was presented at its
write, and rowDone is main's storedMessageDone. main's general split of a
batch that resumes a logical turn (its continuations alone, every other
wake back to the queue) replaces this branch's narrower split while
arrivals waited, and the autoContinue give-back beside it goes, as main's
split leaves no mark there.

The rest are unions: the imports and fields of both sides, main's manager
release beside forgetPresentation (as abec840 wired it), the subconscious's
voice with main's model variable, the hand-off's vouching beside main's
streamCarried, the puppet's own-row pair with main's stored flag, the
guard's notices with its own-row sink, the history search's matched count
with its caller view, and agent.ts's exported branchKey and main's
branchHead, which reads the store's head through a new
read-marks.storeHeadOf, as pendingWatermark is a ReadMark here. The
refusal rewind takes main's carried rule onto the window shed (only what the
refused request carried, a sanitizer-empty row passed over, and a held
result going with its carried call), keeps the shed's doc comment, and the
refused reply's removal leaves every other window. main's new cut-off
tool-block notice is written through storeOwnRow and the writers' guard
lists it (decision 7). changelog.d/context-manager-0.22.breaking.md takes
main's deletion: 0.27.0 released it, and 291-chronicle-0.5 carries the
^0.23.0 range.

Tests. The conflicted ones: heard-mid-turn-wake's and mcpl-coalescing's
stored-message wakes read readsRow; silent-heartbeat-rows takes main's
heldRestartMeets helper with this branch's expectation (a resumed turn
presents no arrival, so the message's wake starts its own turn), and
main's five further held-restart cases are read onto that;
stop-with-host-operations keeps main's refused second switch and this
branch's switch started as the first returns; stop-with-work-in-flight
drops main's persisted-queue record case; turn-continuation adds main's
no-further-turn check. test/reinit-after-branch-move.test.ts (#353) is read
onto this branch, a header saying how: the undo and redo cases are read
onto the window, the cases whose point is a move that holds the store make
it with switchBranch, held writes are '' or a placement with no id, and its
alert texts follow the new remedy; the fix wave's six window cases are
added to it as a describe of their own, and test/branch-move-resync.test.ts
is removed. Two of main's refusal-rewind cases read the window (the result
leaves the window with its call; the message stored while the refused
request streamed follows the marker); reasoning-loss-turn's line follows the
first reply in the worker's window, where it is presented, not in the
record, where it is stored at once; subconscious-model-fields counts
selfParticipants and presentAs among the subconscious's own fields, which
it sets for its own voice; compression-after-release's (#550) fork cases
present the backlog they write past the framework, as the fork's next
point would, since a registered agent compresses its window, and a write
made while quiesced is stored at once. main's changelog fragments for #353,
the undo reservation, settled, the stored-message wake and the continuation
split are rewritten for this branch, as are 291-branch-move-resync and
291-presentation-mandatory.

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 27, 2026
…n's held-turn model

Main now treats a turn a provider hold keeps for its retry as alive
(TURN_RETRY): it counts in activeTurns, drained reads false, it passes a
quiesce, surgery refuses it as agent-busy, and an abandon, stop() or the
agent's removal ends it with one inference:aborted. anima-research#361's budget hold did
the opposite: the held turn gave its token back and read as idle, so a
surgery was admitted while it waited, and the turn was then released onto
the store the surgery had moved. A run at anima-research#361's head 54565d8 showed that:
a rollback past the held turn's message was admitted, and the released turn
answered without that message in its history.

The conflicts sat on that difference (passesQuiesce, gatedRequests, the
scheduler's turn-alive test and trigger selection, the reaper, the policy
drop). They are resolved by taking main's model and making the budget hold
one more place a turn's retry waits:
- the hold keeps the refused start's request as its turn's retry
  (asTurnRetry, with the turn's carry), joins later wakes with holdRequest,
  and the failed start ends as a provider-held start does (endHeldStart);
- pendingContinuationAgents, gatedRequests and heldTurnRetry read the
  budget holds, so the turn is alive, a wake that joined it counts as
  gated, and surgery's and the puppet's refusals name the hold and the way
  on (a quiesce with abandon);
- dropContinuations ends a budget hold with the turn whose retry it holds,
  putting back the wakes that joined it after a caller's stop or an
  abandon, and endContinuedTurn settles a released hold's record;
- the release queues the held requests as they are, and main's scheduler
  merges and passes them.
What goes: releasedByBudgetHold and the passesQuiesce exclusion; the
surgery, undo and redo re-check (checkBudgetHoldsAfterSurgery, and the
storeSurgeries count in the signature), since no store move is admitted
while a turn is held; the check's surgeryHold guards; HostModeStatus's
heldTurns, since the held turn is in activeTurnAgents; the reaper's own
budgetHolds check, which hasTurn covers; and the scheduler's deferred-batch
and guard-retry trigger handling, which main now does for every
continuation.

Main's stored injections (0e2fa84) store every afterUser injection, so the
note a released turn's compile carries is stored too, where the turn reads
it. Its words now say only what happened ("... so this turn waited 7 min
for compression to make room."), true whatever its compile then meets. The
release check's store-change count leaves out the note's own rows: without
that, a released turn whose compile is refused would be released again on
the strength of its own note.

The release check asks the preview, and the fallback dry compile, with the
agent's thinking binding, as main's previews ask (previewContextFor), so
its verdict prices a summary's carriers as the turn's compile does.

kickOverBudgetDrain takes anima-research#353's stop for an undo or redo that moved the
store under a tick, for the breaker's kick and the hold's, each with its
own tag and label.

Tests: those that pinned an idle held agent are turned round (the status
reads it alive; a surgery, a rollback, an undo and a puppeted call are
refused naming it; a quiesce waits for it and its abandon ends it; a
caller's stop ends it), the note tests read the stored note, and tests of
scheduler behaviour main now owns are removed. One test pins the thinking
binding the check asks with, and one that a dry compile resolving doesn't
release a turn the preview says doesn't fit. Context-manager 0.23's dry
compile and preview agree on the real strategy's fixture at every budget,
so the real-strategy test now pins the stored note's margin case instead.

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

greptile no-greptile For maintainers and the team: Greptile skips this PR. Not for general use.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

After a surgery or branch switch, the other agents on the store can't compile until the host restarts

2 participants