Repository navigation
fix: every agent on the store works again after a rollback, suppression, branch switch, undo or redo - #353
fix: every agent on the store works again after a rollback, suppression, branch switch, undo or redo#353nissa-seru wants to merge 46 commits into
Conversation
…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 already running
…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.
[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
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.SummaryThe PR reopens registered agents' context managers after branch moves and keeps the store reserved until recovery finishes.
Reviews (2) · Last reviewed commit: "docs(operator): a manager's re-initializ..." · Reviewed by Greptile |
…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
|
Pushed — Ada-1017 |
Anarchid
left a comment
There was a problem hiding this comment.
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
- Does every agent resume consistently after rollback, switch, undo and redo? For agents registered in
this.agentswhose 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:RecordJournalis branch-independent by design (src/record-journal.ts:1-12). - 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).
- Do the tests exercise real store operations? Yes for every success path.
AgentFramework.createruns on a temp-dir Chronicle store with realContextManagerandAutobiographicalStrategyinstances (test/reinit-after-branch-move.test.ts:31-37,82-99), and only the membrane is mocked. The failure paths patchstrategy.initialize,cm.initializeStrategy,cm.fork, store methods andmaintenancePass. Turn checkpoints are synthesized through the privatesaveTurnCheckpointsrather 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.
createConversationAgentwrites the fork's seed intoconversations/<name>once, at creation, and only when the namespace is empty (src/framework.ts:9724-9738). After that the fork stays inthis.agentsandconversationAgentHomes(9744-9746).reinitializeManagersnow 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
awith one turn checkpointed, fork spawned, thenundoLastTurn('a'). The fork's dry compile went from 2 messages before the undo to 0 after, with the fork still registered and homed onworld:chan. - On
mainthat fork's strategy refused its work (the #326 symptom). Refusing is worse for availability, but it is not silent.
- Probe on the built head: template
-
Ephemeral candidates. Found by Sol; I verified it, and it predates this PR.
createEphemeralAgentkeeps the candidate's manager only in theephemeralCandidatesWeakMap (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.
- dispose a fork whose namespace is empty on the landed branch (
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,
addMessagefor that agent defers (src/framework.ts:15391-15411). - A failed
setStrategyleaves context-manager'sinitializedAttemptnull, soappendMessagerefuses (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.
- While the store is reserved,
- Probe:
undoLastTurn('a')withb's nextinitializefailing, thenaddMessage(..., { 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
finallycallscontextReadiness(cm)(src/framework.ts:3536-3541). The autobiographicalcheckReadinessrefuses on the stale generation (autobiographical.ts:4553-4554). reportMaintenanceFailurethen 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: onecontext-maintenance-failed agent=b: AutobiographicalStrategy.checkReadiness requires reinitialization…alert, thenbready 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.
updateAgentRuntimeSettingscallsagent.updateRuntimeSettingsbefore persisting or logging, with no hold check (src/framework.ts:4337-4400).- A patch with only
sameRoundThinkTextPolicysets the override (src/agent.ts:647-649) and then callsgetRuntimeSettings()→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.mdsays 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.switchBranchselects the branch before it initializes (context-manager src/context-manager.ts:920-923).- So when
restoreSourceBranchcatches an initialization failure, the store is already back onsourceBranch, 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-510drives 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.deleteneither 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 threwBranch not found,redoDepthstayed at 2, andcanRedoreports 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.initializeis synchronous apart from one await. It runsregisterStatesandloadPersistedStatebefore that await, andmessageStore.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 whatbcompiles after a rollback would pin the consistency claim; in this harness,aandbshare a namespace, sobloseshello 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→markStoreBranchSwitchalways advances it (branch-generation.ts:19-28,context-manager.ts:916-924).selectBranchdoes nothing else, sosetStrategyskips no manager-level reset. The tracker is a per-JsStoreWeakMap, 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'sabortIfStalereturns before it commits. Thedo … while (state.again)loop runs again, and only the last pass is reported. - Undo/redo coalescing.
- Nothing can join between
state.againreading false andsyncMoveReinit = null: the code between them is synchronous. - A joined move checks that every agent is idle (
7935-7958). beginSyncMoveprecedes the redo pop, so a refused redo keeps its entry.- The checkpoint save moved under the reservation and is restored on failure (
7624-7637).
- Nothing can join between
- Reservation. It is released exactly once (the
releasedflag,7850-7853), abandoned on a failed move, andstop()waits for it throughtrackOperatorOperation. 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. switchBranchto 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-failedtext 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 catchingErrorfrom 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,cmdBranchSwitchbroadcasts after the await (server.ts:583-611). - Build and tests. Typecheck and build are clean.
test/reinit-after-branch-movepasses 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.
…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>
main 794bf97 into 0c3537c, merge base ca2f806 (1,334 commits). Ten regions in three files; git merged the rest on its own. src/api/server.ts (1): the branch-switch comment takes main's (anima-research#250: a branch switch adds or removes no Discord awareness marks) with anima-research#353's sentence on the framework refusing another move until this one's awaited steps end. test/discord-awareness-outbox.test.ts (1): main's side whole. anima-research#250 replaced the message-granular undo test this PR had given a conversationSpawnsInFlight stub field. src/framework.ts (8): - the host command's undo and switchBranch(): main dropped startDiscordAwarenessDelivery (anima-research#250); anima-research#353's lines stay (the preview waits for syncMoveReinit; switchBranch re-initializes every manager). - the surgery release's flush: main's readdDrained (anima-research#286's helper, which keeps a write it can't store in its agent's queue: R1-2's fix) with main's per-agent pendingAssistantBlocks guard. e4109c2's message:added trace for each write the release stores is kept through a new traceAs option on readdDrained (deferred-flush:surgery-release); the deferred write's id is stored by main's fromQueue, as anima-research#353's deferredWriteId did. - both sides added methods at one place: anima-research#353's reinitializeManagers family, then main's prepareMarkerBatch, refuseMarksWithoutLedger and retireAfterRestore. - rollback's and suppression's failed switch or fork: anima-research#353's moved check stays; main's retireAfterRestore replaces the discard / retireBatch(). - createConversationAgent: main's usedEphemeralAgentNames.set(name, 'conversation') with anima-research#353's conversationSpawnsInFlight.add(name). - anima-research#353's seedConversationAgentIfEmpty, then main's routeConversation, spawnConversationAgent and storeConversationGeneration (anima-research#430/anima-research#428), which replace persistConversationRouterState. Nothing calls the old name. Both spawn records are kept: anima-research#430's conversationSpawns (per channel, for routing's wait, from the routing decision on) and anima-research#353's conversationSpawnsInFlight (by name, for the move refusal's words, from after the spawn's wait for a hold). One could serve both only by changing the map's value and when a spawn counts as busy for a move. Typecheck clean on the merge (tsc 7.0.2).
…ove) main moved by one PR since the merge in bf2e203: anima-research#570 (F296), which changes test/batched-wake-locus.test.ts alone. No conflicts.
main 794bf97 into 0c3537c, merge base ca2f806 (1,334 commits). Ten regions in three files; git merged the rest on its own. src/api/server.ts (1): the branch-switch comment takes main's (anima-research#250: a branch switch adds or removes no Discord awareness marks) with anima-research#353's sentence on the framework refusing another move until this one's awaited steps end. test/discord-awareness-outbox.test.ts (1): main's side whole. anima-research#250 replaced the message-granular undo test this PR had given a conversationSpawnsInFlight stub field. src/framework.ts (8): - the host command's undo and switchBranch(): main dropped startDiscordAwarenessDelivery (anima-research#250); anima-research#353's lines stay (the preview waits for syncMoveReinit; switchBranch re-initializes every manager). - the surgery release's flush: main's readdDrained (anima-research#286's helper, which keeps a write it can't store in its agent's queue: R1-2's fix) with main's per-agent pendingAssistantBlocks guard. e4109c2's message:added trace for each write the release stores is kept through a new traceAs option on readdDrained (deferred-flush:surgery-release); the deferred write's id is stored by main's fromQueue, as anima-research#353's deferredWriteId did. - both sides added methods at one place: anima-research#353's reinitializeManagers family, then main's prepareMarkerBatch, refuseMarksWithoutLedger and retireAfterRestore. - rollback's and suppression's failed switch or fork: anima-research#353's moved check stays; main's retireAfterRestore replaces the discard / retireBatch(). - createConversationAgent: main's usedEphemeralAgentNames.set(name, 'conversation') with anima-research#353's conversationSpawnsInFlight.add(name). - anima-research#353's seedConversationAgentIfEmpty, then main's routeConversation, spawnConversationAgent and storeConversationGeneration (anima-research#430/anima-research#428), which replace persistConversationRouterState. Nothing calls the old name. Both spawn records are kept: anima-research#430's conversationSpawns (per channel, for routing's wait, from the routing decision on) and anima-research#353's conversationSpawnsInFlight (by name, for the move refusal's words, from after the spawn's wait for a hold). One could serve both only by changing the map's value and when a spawn counts as busy for a move. Typecheck clean on the merge (tsc 7.0.2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ssion and disposal cases, and R1-2's deferred write - discord-awareness-outbox: main's hostCommandFramework harness, added after this branch's base, gets conversationSpawnsInFlight, as this PR gave the harness anima-research#250 replaced. Without it the two undo tests read the move refusal's busy check on an undefined set. - reinit-after-branch-move, a suppression refused before the store moved: main now refuses a taken branch name before anything is prepared (7623cf2's known-collision check, code invalid), so the test injects the fork's own refusal and keeps exercising the moved check this PR adds to the fork's catch. - reinit-after-branch-move, a fork whose disposal fails partway after its seeding fails: anima-research#515 made evictTurnCheckpoints log a store that refuses to drop the checkpoints and go on, so that no longer fails a disposal. releaseWindow, which disposeConversationAgent runs unguarded, still can throw, so the test injects that, and the reseed pass's catch and its cleanupFailed words stay tested. - R1-2 (Anarchid's review of this PR): a message deferred during the hold for an agent whose re-initialization fails is still queued for it after the release, and stored once, in its window, after a switch to the current branch recovers it. Red-first for R1-2's test: at 0c3537c (this PR before the merge) it fails at "still queued for b after the release" (the release's flush dropped the write); on the merge it passes, since anima-research#286's readdDrained keeps a write it can't store in its agent's queue. Runs: tmp/chk/326r3/merge-tests-two-files.txt (125 of 125, both files) and tmp/chk/326r3/red-r12-on-0c3537c6.txt (1 of 1 fails as above), under ~/.local/state/ada-1017. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ove) main moved by one PR since the merge in bf2e203: anima-research#570 (F296), which changes test/batched-wake-locus.test.ts alone. No conflicts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ts example Since anima-research#515, a store that refuses to drop a conversation agent's turn checkpoints no longer fails its disposal (evictTurnCheckpoints logs it and goes on), so the fixed fragment's example of a disposal failing partway was no longer true after the merge of main. The sentence keeps what holds: the alert says so, and the move's other conversation agents are still seeded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… after the merge of main From Beth-2389's read of the merge: - cleanupFailed's doc and the re-seed pass's doc named a store refusing to drop the agent's turn checkpoints as how a disposal fails partway. Since anima-research#515, evictTurnCheckpoints logs that and goes on; releasing the agent's window, which disposeConversationAgent does right after it unregisters the agent, still can throw, so that's the example now (and what the disposal test injects). - The suppression fork's catch said a fork is refused before it switches when 'a name already taken'. The known-collision check now refuses a taken name before anything is prepared, so the fork meets one only if the name is taken during the waits after that check; the comment says so. - The release flush's comment: the traces come after the batch's writes, so 'each one stored is traced' rather than 'traced as it lands'. Comments only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
acd4d34 to
7dd0f87
Compare
|
Replying to this review. @Anarchid The branch now merges
A force push at 13:14 UTC rebuilt the five commits after Written by Ada-1017. — Lottie-2661 |
Anarchid
left a comment
There was a problem hiding this comment.
Nightly dual review · round R3 at 7dd0f87 · 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 R3-n are stable; cite them in fix commits.
Reviewed at 7dd0f87 against main f8c23e1
Nightly dual review · round R3 · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged, with 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 R3-n are stable; please cite them in fix commits.
Scope.
- The PR's own diff against its merge base
8225e4bf:src/framework.ts, a comment insrc/api/server.ts, the two326-*fragments and four test files. - The ten commits since round 2 (
1d33acd1..7dd0f878), read line by line. Two of them are merges ofmain. mainhas moved 179 commits past8225e4bf. This branch merges into currentmainwithout conflicts, and that trial merge was built and tested as well as the head.- Dependencies: context-manager 0.14.1 (what
^0.14.1resolves to today), chronicle 0.4.0 and membrane 0.5.87.
On the nomination's stated facts.
- Round 2 is the review posted at
1d33acd, with findings R2-1 to R2-3. That holds, with the same precision as last round: GitHub records the review as a comment, and the verdict line in its body requested changes. - #286 has merged. It merged on October 17 at 19:51 UTC, and this branch merged it in at
c8e25ef5and4a0cd8b9.
Verdict: changes requested
This round answers round 2 completely:
- The four carried findings are fixed. R2-1, R2-2, R2-3 and R1-2 are each fixed and each pinned by a test. Removing the R2-1, R2-2 or R2-3 fix from the built head fails a test in the new file.
- One major keeps this from approval: R3-1.
- Merged into current
main, the suite fails. A testmainadded after this branch's last merge callsundoLastTurn()and writes at once. - The cause is the contract change underneath:
undoLastTurn()andredo()return while the store is still held, and only this framework's ownhost/commandundo can wait for the hold to end.
- Merged into current
- The minors are smaller. Four are claims in the description or comments that the code doesn't keep, including one more write path past the hold (R3-2 to R3-5). One is a branch-bound subsystem this PR doesn't touch, which the "without a restart" framing passes over (R3-6).
The nomination's questions
- Are R2-1 to R2-3 and R1-2 fixed? Yes, all four, with tests (table below).
- What is wrong with the PR, including what it doesn't claim to touch?
- R3-1: the PR is red against current
main, and a caller of undo or redo can't wait for the hold to end. - R3-2: module edits and removals, and fresh tool results, still reach managers during a hold, which the PR's text says can't happen.
- R3-3: the description misstates what happens to messages held for a fork that the pass disposes.
- R3-4: a re-initialization that never settles holds every agent, and nothing shows it.
- R3-5: the alert's "or a restart" remedy loses the write that R1-2's fix keeps queued.
- R3-6 (not this PR's to fix): the MCPL channel lifecycle and tune-out state are not reloaded after a move.
- R3-1: the PR is red against current
Earlier findings
Line numbers are at this head: code in src/framework.ts, tests in test/reinit-after-branch-move.test.ts.
| id | disposition | evidence |
|---|---|---|
| R2-1 | fixed | writeToFork defers under the hold (:12540-12547). All three direct fork writes take it: the channel message (:12509), the deliverTo correction (:12321-12327) and the idle-TTL closure (:12891-12895). The coalescer's removal is declined under a hold (:13801). Tests: :1466 (message and correction, with placements resolving), :1499 (declined removal), :1517 (TTL closure) and :1534 (rollback's wait). Removing the hold branch from writeToFork fails three of them. Two other paths still write past the hold; see R3-2 and R3-3. |
| R2-2 | fixed | The managers re-initialize one at a time, with a setImmediate before each, the first included (:10248-10260). So none re-initializes inside undoLastTurn(). The test is at :546; removing the yield fails it. |
| R2-3 | fixed, differently from the suggestion | The seed compares the store's branch id before and after the template's compile, and fails on a move it didn't make (:12669-12685). The re-seed then disposes the fork and alerts. The reasoning holds: no later pass follows a move made through an agent's own manager, so treating it as "moved" would have left the fork unseeded. The test is at :1552; removing the comparison fails it. |
| R1-2 | fixed | The release flushes through #286's readdDrained, which puts back into the queue a write it can't store. The test at :270-304 checks that the write is still queued after the release and stored once after the recovery switchBranch(). It passes at the head and on the trial merge. The restart half of the remedy still loses that write; that is R3-5. |
| R1-1 | fixed for forks (round 2); ephemeral candidates tracked in #383 (open) | — |
| R1-3 – R1-7 | fixed (round 2) | R1-7 was narrowed at 83d9e72f to drop only the redo's own dead step, which is what round 1 asked for. |
| R1-8 | tracked in connectome-host#264 (open) | The host's /undo, /redo and /checkout still move the store through one agent's cm.switchBranch(), so they are outside this fix. The description's "Not covered" says so accurately. |
| R1-9 | addressed | Measured in round 2. The fix is R2-2. |
| R1-10 | fixed | The test of a write deferred for an agent whose re-initialization fails now exists (R1-2's). |
Major
R3-1. Merged into current main, the suite fails, because undoLastTurn() and redo() now return before the store is free and no public caller can wait for it. Found by Claude.
- The failing test. On the trial merge with
f8c23e13, the full suite passes 5,657 of 5,701, with 43 skipped and 1 failing. The failure is deterministic when the test is run alone.- The test is
main'stest/mcpl-coalescing.test.ts:1571, "a wake to read a render starts its turn until the render's row is at or below the agent's watermark on the current branch". It was added by3253dab4, after8225e4bf. - At
:1639-1640it callsf.framework.undoLastTurn('agent'), then lands a queued render throughreaddDrainedand asserts that its row is stored. - Under this PR, the undo's reservation still holds
agent's turn token, soaddMessagedefers the write (src/framework.ts:21309-21315). The test fails on "render_1 landed on the current branch". - With one line added after the undo,
await framework.syncMoveReinit?.done, the test passes.
- The test is
- Why this is more than a test to adapt.
syncMoveReinitis private.- Only the
host/commandundo waits for the pass to finish (src/framework.ts:9525). - The API server's
undoandredo(src/api/server.ts:649,:667) broadcastbranch:switchedas soon as the call returns. - A client that answers that event with
agent.contextgets a compile run directly on the agent's manager (src/api/server.ts:726). By reading, an autobiographical agent the pass hasn't reached yet refuses that compile with context-manager's "requires reinitialization": the #326 symptom. It lasts until the pass ends, which the description measured at 4.5 to 10.9 s with 16 to 32 agents. - A host that writes right after an undo sees its write deferred, as the test above did.
- The fragment documents that the store stays held, but it gives callers nothing to await.
- Only the
- Fix.
- Required: merge current
mainand adapt that test. - Recommended: give callers a public wait. That could be a
whenStoreReleased(): Promise<void>, or asettledpromise inundoLastTurn()'s andredo()'s results, so they stay synchronous. HavecmdUndoandcmdRedo, which are alreadyasync, await it before they broadcast, and use it in the tests in place of the private field.
- Required: merge current
Minor
R3-2. Some writes still reach a context manager while a move holds the store, so the hold's "nothing touches a manager's stored messages" is not true. Found by both reviewers independently (Sol also found the tool-result path).
- Module edits and removals.
ModuleContext.editMessageandremoveMessage(src/framework.ts:2848-2849), and a process response'seditMessagesandremoveMessages(:12192,:12199), go toeditMessageandremoveMessage(:21927,:21945). Those write to the primary's manager without checking the hold.- The in-tree callers are Discord's live edit and delete (
src/modules/discord/index.ts:884,:902), and its history sync (:1174,:1206), which runs after network awaits.
- The in-tree callers are Discord's live edit and delete (
- Fresh tool results.
addMessageexempts any new write that carries atool_resultblock from every deferral, including the reservation's turn tokens (:21297,:21310).- No in-tree module writes one outside a turn, but
ModuleContext.addMessageandEventResponse.addMessagesaccept one.
- No in-tree module writes one outside a turn, but
- What it costs. During a rollback's or suppression's wait for running maintenance, before its switch, a Discord delete or edit is applied on the source branch. The rollback then branches from an earlier point of that branch, so on the branch it lands on, the deleted message is back. That is the same shape as R2-1's point 2, which this PR fixed for added messages.
- The wait comes from #352, so the loss itself predates this PR.
- What is this PR's own is the claim: the
removePlacementcomment (:13793-13799) and the description's "Nothing removes a stored message while the store is held".
- Fix. Either of:
- Under
surgeryHold, decline module edits and removals asremovePlacementdoes, or queue them for the release. Let the hold override the fresh-tool-result exemption too: a reservation means no turn is live, so no continuation depends on that write landing. A test with a process response's removal during a rollback's wait for maintenance would pin it. - Narrow both texts to the coalescer, and file the rest.
- Under
R3-3. A fork disposed by the re-seed pass doesn't have its held messages "dropped by the release's flush": they are written into its abandoned namespace during the hold. Found by Sol; verified.
- The path.
disposeConversationAgentunregisters the fork and callsreleaseWindowNowOrAfterResume(:12809). Unless the host is quiesced, that runsreleaseWindowat once, while the hold is still up.releaseWindowdrains the fork's queue and writes each entry directly into the fork's manager (:21640-21646).- If such a write throws, it is logged, and the
ackDeferredWrites()that follows forgets it (:21647-21662).
- The outcome. Either way, no agent reads the message again. A user message that reached the channel during the hold doesn't reach the fresh fork the channel's next message spawns. The description's last "Not covered" bullet says it is dropped at the release with a log line.
- Fix. Correct that bullet. Better: hand those writes to the channel's next spawn, or have the disposal alert say how many held messages went with the fork.
R3-4. A re-initialization that never settles holds every agent, with no alert and no status. Found by Sol; narrowed here.
- The wait. Each
await cm.setStrategy(cm.getStrategy())(:10255) has no deadline. The store is released only after the whole pass settles. - The silence. A strategy whose
initialize()never resolves raises nocontext-reinit-failed. Every turn waits, and messages pile up in a queue kept only in memory.- The hold records
since(:10189), butgetHostModeStatus()(:6067) doesn't expose it, and neither does the health snapshot.
- The hold records
- What predates this PR. Surgeries already had this property through
cm.switchBranch(). This PR extends the hold to undo and redo, and its length to one initialization per agent. - Fix. Expose the hold (
verb,agentName,since) in host status and health. Log, or alert, when a pass runs past a threshold. A deadline that releases the store is not safe without a cancellation contract in context-manager, so it isn't suggested here.
R3-5. The alert's "or a restart" remedy loses the write that R1-2's fix keeps queued. Found by Claude, by reading.
REINIT_REMEDY(:1810) offersswitchBranch()"or a restart".- On a restart.
stop()'s last flush stores past the deferral (:3986). An agent whose initialization failed still refuses that write, sowriteDrainedInOrderputs it back into the queue, logs the failure, and the process exits.- Unless the queue is persisted, which in normal operation it is only while the host is quiesced, the write is gone.
stop()'s own warning about writes lost this way covers only agents whose stream is still running (:3988-3998).
- R1-2's test covers the
switchBranch()recovery only. - Fix. Either of:
- Persist the queue when a release keeps writes for an agent whose manager refuses them.
- Have the alert say that queued writes survive a
switchBranch()recovery and are lost on a restart. Extend the stop warning to cover them too.
R3-6 (predates this PR; not its to fix). After any move, the MCPL channel lifecycle and tune-out state stay as they were on the branch the store left, until a restart. Found by Sol; verified on main f8c23e13.
- Where it's loaded.
ChannelRegistryreplays its branch-scoped lifecycle log (open, closed and tuned-out desired states, and wake counts) only in its constructor (src/mcpl/channel-registry.ts:1351,:2263-2310). It appends later records to whichever branch is current (:2320-2322). - Tune-out.
TuneOutCoordinator.resumeActiveEpochs()runs only at framework creation (src/framework.ts:3373). - What this does. After an undo to before a tune-out, the channel's new traffic is still diverted and stamped as tuned out until the host restarts.
- What this PR touches.
reinitializeManagersre-initializes context managers and re-seeds forks, and nothing else. So "works again without a restart" holds for the context managers, which is this PR's scope, but not yet for the host as a whole. - Ask. Scope the description's phrasing to the context managers, and file this separately. Reloading branch-scoped module and MCPL state is a move participant of its own.
Verified fine
-
Tests.
- At the head:
reinit-after-branch-move,conversation-routing,stop-with-host-operationsanddiscord-awareness-outboxpass 217/217. The trial merge also passes those files 217/217; its only failure is R3-1's. - CI passes all six checks at the head.
- At the head:
-
Mutation checks at the head. Each fix below was removed on its own, and each removal fails a test:
writeToFork's hold branch: 3 tests fail;- the yield before each manager: 1 fails;
- the seed's branch comparison: 1 fails.
-
Store-mover inventory.
- Every move the framework makes,
switchBranch,undoLastTurn,redo,rollbackToMessageandsuppressMessages, reserves the store and re-initializes the others. - The API's
branch.createandbranch.deletedon't move the current branch. Chronicle 0.4.0 creates a branch without selecting it, and refuses to delete the current one. src/recovery/offline-branch.tsuses its own offline manager.- The trial merge adds no live mover.
- Every move the framework makes,
-
Direct writes to a context manager, in the head and in the trial merge. Apart from R3-2 and R3-3, every direct write happens inside a turn, a tool cycle or its teardown, which a reservation excludes. The rest are:
- the seed, which writes on purpose;
- the suppression's removal, which is the operation itself;
- the release's flush.
main's new writers since8225e4bf, such as the reply-open-failed notices, go throughaddMessageand are deferred.flushForStartruns only under a turn token. -
Release and error wiring.
- Switch, rollback and suppression release in nested
finallyblocks. - The undo and redo pass releases in its own
finallyand ends in acatch. - There is no await between the last read of
againand the clearing ofsyncMoveReinit. maintenanceTick()refuses to run under a hold. The idle-TTL reaper can't dispose a fork during a hold, since the reservation holds the fork's turn token.
- Switch, rollback and suppression release in nested
-
The disposal-failure fix for Greptile's finding (
0c3537c6): the unbind and the disposal share one catch per fork, the failure goes into that fork's record, and the pass moves on to the next fork.
Method. Two independent passes, merged after re-verification.
- Codex GPT-5.6 Sol (xhigh, read-only sandbox, no network):
- Ran about 17.5 min and 88 commands, using 10.92M input tokens (10.47M cached) and 49.9k output (29.7k of it reasoning).
- Read this head, the trial merge with
mainf8c23e13, the round-1 and round-2 reviews with the replies, context-manager 0.14.1 and chronicle 0.4.0. - Could not run tests, and could not check CI or the state of any issue.
- Claude Opus 5.5:
- Built the head and the trial merge, and ran the full suite on the merge and the touched files on both.
- Isolated R3-1's failure, and confirmed it with a one-line probe that awaits the pass.
- Built three mutants, one for each of R2-1, R2-2 and R2-3.
- Read connectome-host's
mainfor R1-8. - Checked the state of #286, #326, #383 and connectome-host#264, and CI.
- Who caught what.
- Both: R3-2's module edits and removals.
- Sol only: R3-2's tool-result exemption, R3-3, R3-4 and R3-6.
- Claude only: R3-1, R3-5, and the mutation and test runs.
- Severity adjustments. Sol rated R3-2 (two findings), R3-4 and R3-6 majors; they are minors here.
- R3-2: its lossy window is the wait for maintenance before a rollback's switch, which predates this PR, and no in-tree module writes a tool result outside a turn.
- R3-4: it needs a strategy whose
initialize()never settles, and surgeries already had that property. - R3-6: it predates this PR and is outside its scope.
- Not reviewed: connectome-host#264 itself, and the merge with whatever
mainis when this lands.
…ove) main moved by 32 pull requests since this branch's last merge of it (8225e4b, in 4a0cd8b). Only src/framework.ts is changed on both sides, and git merges it without conflicts. Main's hunks come within 40 lines of this branch's in two places, each read in the merged file: - removePlacement: anima-research#607 settles a write whose landing is unknown before the placement resolves, and removes a deferred write from the queue only when its landing is known. This branch's hold check comes after both, before a stored message is touched, so under a hold a write still queued is withdrawn from the queue as before, and a stored one, including one the settle found landed, is declined. - readdDrained: anima-research#607 reworks writeDrainedInOrder beneath it. This branch's traceAs option traces what that returns as stored, after it. main's test/mcpl-coalescing.test.ts:1571 (3253dab, after 8225e4b) writes right after undoLastTurn(). On this branch that write is deferred while the store's reservation holds for the managers' re-initialization, so the test fails at this merge. R3-1 of the maintainer's round 3 adapts it, in the commit after this one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r's events wait for it (R3-1) R3-1 of the maintainer's round 3. With this branch's reservation, main's test/mcpl-coalescing.test.ts:1571 fails, as the maintainer's trial merge found: it writes right after undoLastTurn(), and the store's reservation for the managers' re-initialization defers the write. No public caller could wait for that reservation: only the host/command undo could, through the private syncMoveReinit. The API server broadcast branch:switched at once, so a client that answered by reading an agent's context compiled on a manager not yet re-initialized. - undoLastTurn()'s and redo()'s results carry `settled` when the store moved: the re-initialization's `done`, which settles once the reservation is released and never rejects. beginSyncMove's landed() returns it, the running pass's for a move that joins, so an undo and a redo made back to back settle together. - The API server's undo and redo await it before they broadcast branch:switched and answer. Their answers leave the promise out, so the JSON they send is as before. - The host/command undo awaits its undos' `settled` rather than the private field. - main's test awaits the undo's `settled` before its write. The tests that waited on the private field now wait on the move's `settled`; those that check whether a re-initialization is running still read the field. A new test answers the API server's events as a client would, reading the other agent's context, and checks that each event comes once the store is let go and that the answers carry no promise. - A fragment of its own, `added`, for `settled`; the `changed` one says the result carries it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…is said, and a disposed fork's held writes are counted (R3-2 to R3-6) R3-2 to R3-6 of the maintainer's round 3. - R3-4: no turn starts while a move holds the store, and nothing bounds what the hold waits on: maintenance already running, the move itself, and every manager's re-initialization. A hold still up after 60 s (HOLD_ALERT_MS) raises one `store-held-long` ops alert, for every move that holds the store: an undo or redo, and main's rollback, suppression and branch switch. It names the move and the agent it was for, and the manager a re-initialization pass is on or the maintenance work it waits for, and says what a restart costs. The timer is armed when the store is reserved (reserveStoreForSurgery) and cleared at its release. Tests: an undo and a redo joined, with the second pass held past a shortened threshold, raise one alert naming b's manager; a rollback waiting for maintenance says so; a branch switch held by its config mount after its pass names no manager; and an undo let go before its threshold says nothing, then or after. - R3-5: the remedy in context-reinit-failed, and in a failed restore's alert, says that a switchBranch() recovery stores the writes kept queued for the agent, and that a restart loses them unless the deferral queue is persisted. stop() says, once per agent, how many writes its last flush left queued because the agent's store didn't take them, and whether the queue keeps them for boot. A test checks both, and the two failed-restore tests check their alert's remedy. - R3-3: a fork disposed because it couldn't be seeded takes the writes held for it into its window, where its release stores them. Its alert now says how many, since the fork its channel spawns next doesn't get them. A test checks the write in its window and the alert's count. - R3-2: removePlacement's comment says what this branch 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 fresh tool result, still reach a manager then, which the comment now says. - R3-6: the fixed fragment's headline scopes "without a restart" to the context managers. - The fixed fragment says what a host now sees: the restart's cost, the stop() line, the new alert and the disposal's count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… order the webhook keeps, and a spawn into a stop() fails The fixes from this round's review (at 8249334), as the PR's author ruled them, and a reader's wording point. - The long-hold alert names a manager only while its re-initialization runs. The pass set the name at each manager and cleared it after the last, so through the turn of the event loop before each next manager, where an overdue timer runs, it named the one that had finished. The name is now set and cleared around each manager's setStrategy, and the re-seed and what follows name none. - The alert names the move by the noun the move's other alerts use (MOVE_NOUNS): "The rollback for a", not the hold's verb "roll back". - stop()'s two queued-write lines, the new one and main's line for writes left behind a stream still running, said "the persisted queue keeps them for boot" whenever the queue was mirrored. A mirrored queue whose last write failed or met the cap is behind (deferredWritesFellBehind), and boot reads what the record last held. While deferredWritesBehind is set, both lines now say the record is behind, so a restart can lose them. The long-hold alert's pointer to that line says it isn't given on a quiesced host, which persists the queue. - The remedy says a switchBranch() recovery stores the writes kept queued at resume on a quiesced host, where the release defers them again: in REINIT_REMEDY and in a failed restore's alert. - Both re-initialization alerts say what happened first, then what becomes of the writes and what happens next or what to do, and the cause last. The webhook keeps a long message's start and end, 500 characters (opsAlert), so a long cause no longer cuts the disposal or the remedy out of the post. The console, failures.log and the trace keep 2,000 characters, cut the same way. - A disposed fork's held writes go with it, and its window's release stores them there, now or at resume on a quiesced host, which the alert now says rather than that they're already in its window. When the disposal's cleanup failed, the alert still counts them, and says they may be in its window or still queued under its name. - A spawn that comes to create a conversation agent once stop() has begun, as one waiting out a move's hold can, fails as a failed spawn does (stopHasBegun), rather than open its manager on a store stop() is tearing down. A spawn already past that point when stop() begins is main's race, named in the description. - The review's point on the re-seed's skip didn't hold: a suppression refuses to remove an agent's last message (planSuppression) and a rollback keeps its tail, so the agent either names is never empty after its move, and the re-seed's skip of it never changed an outcome. The skip, and the parameter only it read, are gone; the comment now says why the agent is never empty. Tests: - The test of what the alert names calls the reporter once the pass is at its gate, and the config-mount test once the pass has ended, rather than timing either against the threshold. The rollback's test runs the timer and checks it fires once. The hold-let-go test asserts only that nothing comes after the release. - The alert counts a write deferred during the hold, and the disposed fork holds two. - Between two managers' re-initializations, the hold names neither. - Each wording of each stop() line: memory-only, a current record and one behind (reinit-after-branch-move, stop-with-work-in-flight). - A redo's dropped step with no step beneath it, and with two. - The spawn-waits test holds the template's re-initialization at a gate it opens once the spawn is waiting, in place of a 300 ms delay. - An undo whose release throws still settles `settled`, and the release's failure is logged. - A spawn that waits out a hold into a stop() fails. - A disposal whose cleanup fails, with a write held for the fork. - The assertions on both alerts follow their new order. The fixed fragment says the same: the alerts' order, the remedy on a quiesced host, both stop() lines, the held writes' release, and a spawn into a stop(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a spawn's check is for this lifecycle's stop(), and a failed restore says where the agent serves The fixes from this round's second review (at 6166224), as the PR's author ruled them. - The spawn's check is `this.stopPromise`, a stop() of this lifecycle, which start() clears after a completed stop and a failed stop clears unless it closed an owned store. stopHasBegun() also reads the event queue's closed flag, which never clears, so after a stop() and a start() every spawn was refused as stopping. Whether that lifecycle works is anima-research#315's question, not this check's. - The webhook keeps 345 to 349 characters of a long message's start, by its length, and the rest of its 500 from the end. Each re-initialization alert's outcome and what to do now fit in that start, in every shape the code builds (at most 339 characters, for a branch switch's disposal whose cleanup failed, with the router's remedy). REINIT_REMEDY is shorter; the disposal says what happens next right after its outcome, before the held writes; its outcome sentence and the two template remedies are trimmed. Where branch names run long, a failed restore's last words can still be cut. Tests post the alerts with a 3,000-character cause through a stubbed webhook and check what it keeps. - 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 a stop before the resume leaves them queued for the restarted host to drop when it resumes (a reader's run). - The long-hold alert no longer points at stop()'s line: on a quiesced host stop() still gives the streaming agents' line, and a persisted queue's record can be behind. It keeps the restart's cost. - A suppression whose removals fail after its fork has a current manager on the failed branch. When its restore can't select the source, the status and the alert say the agent serves from that branch, which may be partly suppressed, until a switchBranch() moves it (restoreSourceBranch's managerCurrent), not that it refuses its work. Tests: a spawn after a stop() and a start() (a borrowed store); a refused spawn leaves no name reserved and no spawn in flight; a suppression's hold is named "the suppression"; the suppression whose restore can't select the source, whose agent still compiles; both webhook posts above; the alert assertions follow the new texts; the first long-hold test's title says it calls the reporter. The fixed fragment says the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 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>
|
Replying to this review. @Anarchid The round for your third review is at
Beyond your points, from our own reviews of the round and a reader's (
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>
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()orfork(), and re-initializes no other. So:rollbackToMessage()orsuppressMessages(), every agent but the one named kept a strategy loaded for the branch the store had left;switchBranch(),undoLastTurn()orredo(), which switch the store directly, every agent did, the one named included.An
AutobiographicalStrategythen 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. AswitchBranch()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()andsuppressMessages(): 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()andredo()stay synchronous, since the API server, thehost/commandundo 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 madeswitchBranch()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 carriessettledwhen 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'sundoandredoawait it before they broadcastbranch:switchedand answer (the maintainer's R3-1). It waits first for maintenance already running, a pass or an over-budget drain kick, so nosetStrategyoverlaps 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 onmain. 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-turnhost/commandundo, or an undo and then a redo from the web API.host/commandturn undo waits for that re-initialization before itslastVisiblepreview, since the preview compiles. Onmainthe preview failed, and the command answeredlastVisible: null.failures.logand 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, asopsAlertdoes for any alert without a key of its own. Each alert opens with what happened and what to do or what happens next, in at most 338 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, what becomes of the agent's writes, and its cause last, since a cause can be an error's whole diagnostic. The remedy says that aswitchBranch()re-initializes the manager and then stores the writes kept queued for its agent, at resume on a quiesced host, and that a restart re-initializes it too but loses those writes unless the deferral queue is persisted. A restart creates the configured agents again, butstop()ends each conversation agent and each run, so for a conversation agent the remedy says a restart ends it and its channel's next qualifying message spawns a new fork, and for a run, that a restart ends the run. An agent that has left by the time its re-initialization or its seeding fails, as a run its watchdog ended, isn't reported, since it refuses no one's work. For a failed restore, which an agent is is taken when its rollback or suppression reserves the store, before any of the surgery's awaits.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, andmain'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.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, andmain's rollback, suppression and branch switch (R3-4).switchBranch()re-initializes it. Back on the source, a restart re-initializes a configured agent too. Still on the failed branch, a restart would re-initialize it there, on the branch it shouldn't run on, since a manager works on the branch the store is on, so the alert says so and puts what a restart loses after its opening. A restart ends a conversation agent or a run. 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 aswitchBranch()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.message:addedwith 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 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 by reading 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.ContextManager, the seed writes nothing and fails, naming the branch the store moved to, so the re-seed disposes the agent as below, rather than writing one branch's context onto another. By reading, a spawn fails the same way: the seed refuses before it writes anything, as the re-seed's test shows, and any failure of the seed fails the spawn and clears its name, as "a spawn that fails clears its name" shows with a failing template compile.context-reinit-failedalert says so. When its seeding failed, the channel's next qualifying message spawns a fresh one, unless its seeding fails again. When the template is the cause, the alert gives the template's own error, or says that no agent is registered under its name. Since a spawn seeds from the template too, the channel spawns a fresh one only once that's put right, and the alert says how. Its messages stay on the branches that hold them, but nothing reads them again: the next spawn is a new conversation agent under a new name, so a redo doesn't bring the disposed one back. 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 the move held for it goes with it: its window's release stores what its store takes of those writes there, where no agent reads them, and passes any failed reply on to the primary. On a quiesced host the release is postponed to the resume, and astop()before the resume drops all but a failed reply, logged, asmaindoes since fix(stop): end each live conversation fork as its release would #616 for any release a quiesce postponed, 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 alert says how many, since the fork the channel spawns next, under a new name, doesn't get them (R3-3), and which of these the disposal met. When the disposal's cleanup failed, the alert still counts them, and says they may be in its window, still queued under its name, or in neither, as far as the cleanup got, and that a flush that meets one still queued passes a failed reply on to the primary and drops anything else, logged.stop()of its lifecycle has begun, under way, done, or failed with nostart()since, as one that waited out a hold can, fails as a failed spawn does, rather than open its manager on a storestop()is tearing down or has torn down. Astart()begins a new lifecycle, whose spawns go through.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 withOperatorActionError(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.What callers will notice
undoLastTurn()andredo()are refused withOperatorActionError(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 plainError. 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 withOperatorActionError(failed). By readingmain, a failed undo or redo lost its checkpoint or step there, a redo whose branch was gone included.undoLastTurn()orredo()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, androllbackToMessage(),suppressMessages()andswitchBranch()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 withOperatorActionError(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.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.context-reinit-failed. Its remedy says what a restart costs, andstop()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.stop()of its lifecycle has begun, under way, done, or failed with nostart()since, fails, and is reported as any failed spawn is; the channel message that started it is dropped with that spawn.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 andredo()'s results carrysettledwhen 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.undoandredoanswer, and broadcastbranch: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.context-maintenance-failed.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.
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 secondswitchBranch()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.setImmediateper 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()andredo()stay synchronous (beginSyncMove,reinitializeAfterSyncMove,syncMoveReinit). The API server, thehost/commandundo 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 theagainflag and the loop that runs the pass again. Without the join, the second turn of a multi-turnhost/commandundo, or a redo right after an undo, would be refused as busy.settledon undo's and redo's results (the maintainer's R3-1) is how a caller waits for the reservation those calls leave held: before it, only thehost/commandundo 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 aswitchBranch()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 inreserveStoreForSurgeryand 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.seedConversationAgentIfEmpty, now shared with creation, andreseedEmptiedForks): without it, a conversation agent spawned after the point the move returns to answers with no history. Weighed: disposing every such agent and letting its channel spawn a fresh one. That loses each one's conversation across an undo and a redo, since a redo can't bring a disposed agent back. Seeded again, it keeps its name, and the redo finds its history. Disposal stays for one that can't be seeded. Its alert counts the writes that went with it (R3-3): its window's release stores what its store takes of them, where they reach no one, and passes a failed reply on to the primary.cleanupFailed, one catch per conversation agent, for Greptile's review of7bfa5622). Without it, one fork's disposal that threw escapedreinitializeManagers, so the forks after it weren't judged and no failure was reported. It costs a field on the failure and a catch.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.stop()of its lifecycle, after its wait for a hold (stopPromise, orrunningcleared with the queue closed):stop()doesn't wait for a spawn, so one that waited out a hold into astop()would open a manager on a store being torn down, or one after a failedstop()on a framework that no longer runs.stopPromiseis set fromstop()'s first steps, which can emit traces while the framework still runs, and is cleared by a failedstop()and by anaddModule()after a completed one;runningcleared with the queue closed covers those, until astart(). It costs one condition.stopHasBegun(), the operator calls' rule, reads the closed queue whether or not astart()came after, so it would refuse every spawn after astop()and astart().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.refusedWorkAlert,restoreAlert,disposalAlert, andlongHoldAlertforstore-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 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 againstopsWebhookText, the cutopsAlertposts.REINIT_MOVESandTEMPLATE_REMEDIEStype the moves and the template remedies, 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 two typed lists, and shorter remedies.store-held-longnames 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.AgentKind,agentKind,restartClause,refusalDetail, and the failed restore's status): a restart creates the configured agents again, butstop()ends each conversation agent and each run, and the router's bindings are in memory. So for a conversation agent the remedy says a restart ends it and its channel spawns a new fork, which gets none of its queued writes, and for a run, that a restart ends the run; a move's pass re-initializes a run's manager too, since it's registered for its run. Without the kinds, a run was told a restart re-initializes it, andstore-held-longsaid a restart re-initializes every manager. A failure of an agent no longer registered isn't reported, and one still registered is read for its kind then, since every map the kind is read from changes together with the registry, with no await between. A failed restore's kind is taken when its surgery reserves the store, since the restore comes after the surgery's own awaits, during which a run can end. The re-seed's kind is'conversation', written out, since every agent it judges is one, and a fork that left during its seeding isn't judged; read from the maps instead, a fork disposed meanwhile would be named configured. The kind is kept on the failure, since a fork can be disposed before the failure is reported. It costs a three-way type, a method that uses the house's test for an agent a restart doesn't create again (holdProviderAcceleration's), a field on the failure, two registration checks, a parameter of the restore, and a branch in each clause.restartClause'selsewhere, and the restore's status): 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, with no refusal and no alert. It costs a parameter and a sentence after the opening, where the restart's loss of the queued writes moves so the opening fits the webhook's start.restoreSourceBranch'smanagerCurrent): 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.restoreAlerttakes 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'smove: 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.STORES_QUEUED_WRITES: the one wording of how a refusing manager's queued writes get stored, whichrefusedWorkAlertandrestoreAlertboth give, so the two remedies can't drift apart. It costs a constant.releasePostponed, read right after the disposal, on either of its paths (releasesAfterResume): the alert says what the fork's window's release did with the writes held for it, or, when a quiesce postponed it, what the postponement means, which holds whatever follows: the resume stores them, and astop()before it, or the restarted host's resume after a crash, drops all but a failed reply. A failed cleanup that released nothing says where the writes may be. Read later, an empty map wouldn't tell a resume that stored them from astop()that dropped them or a release that threw. It costs a field on the failure.config,Queue,queuedForForkandnotices, adapted from Naomi-2422's probe): the tests build a host, and a second one on the same store after astop(), and follow the writes held for a disposed fork across a quiesce, a resume or a stop, and a restart, by what the queue holds for the fork and what the primary is told. Without them, each variant would repeat that setup.initialized,compiled,endedand the rollback test'send): a test that injects a failure, or an agent's leaving, at one point records that the point was reached, and the run's tests say where the run left. Without them, a test whose injection the code no longer reaches would pass without exercising what its title says. They cost a flag each.stop()ends while it's seeded again has been disposed already, so its seeding's failure isn't reported, and it isn't disposed a second time; a test counts its disposals. It costs one condition.moved): a seed whose store another mover moved fails, and a pass that a joining undo or redo made stale writes nothing, so a seed never writes one branch's context onto another.writeToFork, the maintainer'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, so a placement given while the write was deferred resolves to the stored message. 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 onmain(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.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.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.context-maintenance-failedfor a failure the move caused. The hold starts no tick, so only one already running is treated this way.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 the review of round 3 (
61662246): the re-seed's skip of the agent a rollback or suppression names, and the parameter only it read. A rollback keeps its tail message and a suppression refuses to remove an agent's last one, so that agent is never empty after its move, and the re-seed's own check of stored messages already left it alone: the skip never changed an outcome.Cut by this 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 whatmaindoes, 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.tsputs two agents with autobiographical strategies on one store. It checks that:rollbackToMessage()on one agent, the other compiles and takes a turn, and compiles what the landed branch holds;suppressMessages()on one agent, the other compiles;switchBranch(), and after a switch to the branch the store is on, both compile;undoLastTurn(), the store stays reserved until every manager is re-initialized, and then both compile;redo()likewise;host/commandturn undo answers with its preview, and ahost/commandundo of two turns undoes both;context-reinit-failedalert 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;createBranchAtthrows, and a redo whoseswitchBranchthrows, 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;stop()waits for an undo's re-initialization, and a turn asked for while the managers re-initialize starts once the store is let go;undoLastTurn()returns, and the event loop runs before the last of them has;branch:switchedonce 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;store-held-long, from a hold's reporter: an undo and a redo joined, with the second pass at a gate and a write deferred, name the undo, b's manager and the write; a branch switch held by its config mount after its pass names no manager; between two managers' re-initializations the hold names neither;store-held-longfrom its timer: a rollback waiting past a shortened threshold for maintenance already running raises it once, saying so, and an undo that lets the store go raises none after that;context-reinit-failed, and in each failed restore's alert, says what a restart costs, with each alert's cause last, andstop()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, andmain's for writes behind a stream still running (intest/stop-with-work-in-flight.test.ts), each in its three wordings: a memory-only queue, a persisted one whose record is current, and one whose record is behind;settled, and the release's failure is logged;stop()fails, as a failed spawn does, and leaves no name reserved and no spawn in flight; one after astop()and astart()on a store the framework doesn't own is made;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, andstore-held-long's counts of nine digits, keeps its opening in whatopsWebhookTextposts,store-held-longsaying what a restart does for each move, and each refusing shape, for each kind of agent, says what a restart does to it, in the wording for where the store is, and what its refusal covers;stop()before the resume drops the channel writes with a log line each, stored nowhere, in the primary's window or the fork's, and keeps the failed reply for the primary, which the restarted host tells at its resume; after a crash, a host booted from the directory as it was on disk right after the disposal drops the channel writes at its resume, with a log line each, stored nowhere, and tells the primary of the failed reply; and one whose cleanup then fails, after its release was postponed, has an alert that says so, and the resume takes the writes held for it (adapted from Naomi-2422's probe);stop(), or after a completedstop()and anaddModule(), is refused, and one after astart()is made; a spawn started fromstop()'s first step, while the framework still runs, is refused;The tests wait for an undo's or redo's re-initialization on its
settled, where they read a private field before, and an undo and a redo made back to back share one. A test waiting for something to happen does it throughuntil(), bounded at 10 s, so it fails rather than hangs when that never comes. The test of a fork created while an undo's managers re-initialize holds the template's re-initialization at a gate it opens once the spawn is waiting. Intest/mcpl-coalescing.test.ts,main's test that writes right after an undo awaits itssettledfirst.In
test/conversation-routing.test.ts, the seed test pinned that a spawn's emptiness check on the new namespace was a dry compile. That check is now the namespace's stored message count, so the test pins that nothing compiles the new namespace while it holds no message. Its window takes in the fork's first turn, whose compile comes after the seed.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 this head,
fcb94144, which changes one test's assertion from35785770: the test of a rollback waiting for maintenance allowed no margin between the clock the hold is measured by and the one its timer fires on, and CI's Node 20 run on Ubuntu read 999 ms for a 1,000 ms threshold. Its test file andtest/discord-awareness-outbox.test.ts, run on its tree before it was committed and built from an emptydist, pass 161 of 161, and typecheck and build are clean.At
35785770, on Linux, on a clean checkout installed fresh bynpm installwith no lockfile and built from an emptydist, as CI does, on Node 24.22.0, with context-manager 0.14.1, membrane 0.5.87 and chronicle 0.4.0:npm test) passes 6,021 of 6,065, with 44 skipped and none failing, and the three files CI also runs on their own pass 130 of 130. Typecheck and build are clean.7dd0f878, the head the third review read, each taken out on its own, each fail a test. They coversettledand the API server's andhost/command's waits for it, the long-hold alert and what it names, each re-initialization alert's texts, their order and their fit in the webhook's start, what each says a restart does to each kind of agent, a disposed fork's held writes in each case, the spawn's check against astop(),stop()'s lines for writes left queued, and the re-seed's checks. The pieces before them were taken out at the heads that added them. The test file imports the alert builders, whichmaindoesn't have, so it doesn't build againstmain's sources whole; the removals stand in for that.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.One measurement from an earlier head, whose turn of the event loop before each manager's re-initialization this head keeps:
1d33acd1the event loop didn't turn while the store was held: 5.1 s and 10.1 s. At93129cb8it 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
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.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 the re-seed then finds that message and seeds nothing.switchBranch()and the surgeries are this framework's ways to move the store, and its own writes are what the hold defers.disposeConversationAgent's work undone, as at its other callers onmain. 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.maina failed write of the conversation router's name counters is only logged, so after a restart a conversation agent's name can come round again, over an earlier one's namespace.ModuleContext.editMessageandremoveMessage, 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 onmain. 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).store-held-longsays when a hold runs long (R3-4).stop()whenstop()begins isn't waited for, as onmain. 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.stop()before the resume drops its held writes with a log line, and passes a failed reply among them on to the primary, deferred again for the restarted host's resume, asmaindoes 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, …" intest/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. This change's test shows it for a disposal, with two held channel messages, each dropped with a log line and stored nowhere, and a failed reply, which the primary is told of.main. This change re-initializes the context managers and seeds conversation agents again, and nothing else (R3-6).Merge notes
mainis merged into this branch, at794bf977inc8e25ef5, at8225e4bfin4a0cd8b9, atea4297abin180b703dand at97ca7ebained249de5, and each was read on its own. Git stopped at four places insrc/framework.tsin the last, and each keeps both sides: feat(code-execution)!: code_execution runs JavaScript in the isolate, and the Python runner goes #608'sscriptResultBriefbeside 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 metadatawriteToForkstores 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 inreaddDrainedfor a target that's no longer registered, beside this change'straceAs.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 ofstop()'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'swithoutHostKeys, besidewriteToFork's call.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 goes to the primary and the rest is dropped, logged, at thestop(), 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 astop()before the resume failed for that, and the disposal alert's sentence and the Not covered point said whatmaindid before. The round after the merge adapts them, and the alert now says only what the disposal's release did or will do. 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, sincestop()ends each fork before them, as it already ended each run.undoLastTurn()'s branch and this change's reservation beside it.Read by Beth-2389 (Claude Opus 5.5), who ran the suite at a trial merge with
mainand 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, through1d33acd1, and her probe of the disposal alert changed what it says about the channel's next spawn. She built0c3537c6, for Greptile's review of7bfa5622, and Greta-2425 read it. She read this branch's merges ofmainregion by region, with their receipts, and her points on their comments are in7dd0f878.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. Duncan-2410 built the round for the third review, and wrote its parts here.