Repository navigation
fix: every agent on the store works again after a rollback, suppression, branch switch, undo or redo - #353
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>
… 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>
…ove) main moved by 605 commits since this branch's last merge of it (97ca7eb, in ed249de). git stops at five places in src/framework.ts and one in test/mcpl-coalescing.test.ts: - Where a channel message, or a correction delivered to a fork, is written into a conversation fork, main stores the clock row due ahead of it (storeDueClock) when the fork has no turn alive, and this branch writes the message through writeToFork, which defers it while a move holds the store. Both are kept, and the clock is stored only when no move holds the store: a deferred message lands at the hold's release, and a clock written now would land in the held namespace ahead of the move's re-initialization and re-seed, apart from the message it precedes. The next message after the hold gets the clock, as a clock that can't be stored is skipped (storeDueClock). main's router.touch(channel) is kept. - createConversationAgent: main's signature, a ChannelRef home, with this branch's wait for a move's hold and its check for a stop that has begun. - The fork's seeding: this branch's seedConversationAgentIfEmpty, with main's changes to the inline seeding moved into it: the template's stored context injections left out by the compile's provenance, its channel and by-name notices left out (withoutPrimaryNotices), and logSeedOmissions' new arguments, and main's wording of the compile's thinking binding and the notices paragraph in its comment. - readdDrained: main's account of what passes to the primary for a target that's gone, with this branch's traceAs option. - test/mcpl-coalescing.test.ts: main's wording of the answered watermark, with this branch's await of the undo's settled. test/reinit-after-branch-move.test.ts gives the router and createConversationAgent a ChannelRef ({ serverId: 'world', channelId }) where it gave a channel id, the router's API since main's change. Checked at this merge's tree before committing: tsc and the build pass from an empty dist, and the six files the merge touches (reinit-after-branch-move, discord-awareness-outbox, mcpl-coalescing, stop-with-work-in-flight, stop-with-host-operations, conversation-routing) pass 490 of 490. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s and a platform constraint A comment stays only when it's a license header, a note on non-obvious behaviour forced by an external dependency, platform, vendor or protocol, a directive, a doc comment defining a public API contract, or an issue or RFC link explaining a constraint the code can't express. The other comments this branch added against main go: their reasons are in the commit messages that added them and in the pull request's description. What stays of this branch's comments: - the public docs of switchBranch(), undoLastTurn() and redo(), which say what a caller meets: the store's reservation until every context manager is re-initialized, the context-reinit-failed alert, and the result's settled; - in the rollback long-hold test, the note that the hold is measured by Date.now() while the timer fires on libuv's clock, each to the whole millisecond (Node's own behaviour). Where this branch had rewritten one of main's comments, main's wording is back, except where this branch's change made it false: the seed's comment, moved with the seeding into seedConversationAgentIfEmpty, says the template's compile is the one dry run, since the fork's emptiness is no longer a compile. The test of two API switches handled together loses main's comment, which described the behaviour that test no longer pins. Only comment lines change: every line this commit removes or adds is a comment line, but for one trailing comment taken off a test's await. Checked before committing: tsc and the build pass from an empty dist, and the six files this branch's tests are in pass 490 of 490. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…at reaches the primary main now passes on to the primary, for an agent that has ended, a channel notice that carries its inputs and a module's write for a run or a fork, beside a failed reply (passesToPrimary): the window's release, stop()'s pass for postponed releases and a flush of the queue all use it. The disposal alert's three held-writes sentences, and the changelog fragment, still said only "a failed reply". Each now names the three kinds: a failed reply, a channel notice the fork could have acted on, or a module write for it. The quiesced-disposal tests hold a channel notice and a module's write beside the failed reply, and check that each reaches the primary at the resume, at the restarted host's resume after a stop() before the resume, and at the restarted host's resume after a crash. The channel the notice says opened is decided open in the store's channel lifecycle log, which a host with no channel registry reads it from, and the store is synced before the crash copy, as a host's sync interval would have long before a crash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nel decision still stands The disposal alert's held-writes sentences and the changelog fragment said a channel notice the fork could have acted on goes to the primary. main passes one on only while the channel's durable decision still stands: open or tuned out for a channel-opened notice, still decided open and not open for a failed open. Otherwise it drops the notice, logged (channelNoticeForPrimary). The sentences now say "a channel notice whose channel decision still stands". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…agent one remedy, and narrow the withdrawn tick Three parts of this change are cut back, and one is narrowed. A conversation agent with no stored message of its own on the branch the store moves to is now disposed, its channel unbound and an alert raised, in place of seeding it again from the template. Per-channel conversation routing is deprecated, and the re-seed carried a template-health path, a check for a store moved mid-seed and a callback threaded through every pass. The disposal runs once, after every manager is re-initialized for the branch the store lands on, with no await inside it: an undo and a redo made before the pass ends judge the branch the redo lands on, so a fork the redo restores is kept. A fork is disposed whether or not its own re-initialization succeeded, and only the disposal's alert is raised for it: its message count reads the store's branch directly, and the refused-work alert's remedy, a switch to the current branch, would dispose it rather than recover it. The cost: an undo and then a redo don't bring a disposed fork's conversation back; the channel's next qualifying message spawns a fresh fork from the template, which needs the template's own context manager to be working. The disposal alert gives the number of writes held for the fork while the store was held, and no account of where they go. That is the window release's behaviour, which this change doesn't alter. A run showed that a module's write held for a disposed fork passes to the primary, and a fork seeded from the template, when the template is the primary, carries it. Every agent kind gets one remedy: a switch to the current branch re-initializes it and stores its queued writes, at resume on a quiesced host; a restart can lose them. That makes no claim about what a restart does to the agent, so the agent-kind threading goes. The refused-work and restore alerts no longer say that a restart ends a conversation agent or a run; the long-hold alert still says what a restart does, as it needs no agent's kind. Creating a conversation agent goes back to judging its namespace empty by a dry-run compile of it, as before this change; the stored-message count was only shared with the re-seed. A maintenance tick an undo or redo moved the store under is withdrawn only when it meets context-manager's refusal of work on a branch it isn't loaded for, "requires reinitialization for the current branch generation" or "refused stale branch-scoped mutation". Any other failure it meets, in the tick or in the readiness read after it, keeps its maintenance-failure alert. The tests follow: those for the re-seed and the per-kind texts go, the disposal's are added, the alert texts are checked as facts rather than whole sentences, setups go through public routes where one helper allows (a real turn records the checkpoint, a module writes for an agent, a held stream makes an agent busy, a held maintenance tick is the running maintenance), and a coalesced update or retraction made while a branch switch holds the store is checked where a caller meets it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a disposed fork's alert gives its failed re-initialization A spawn stored its channel's name counter before it waited out a move's hold. During a rollback's or a suppression's wait for maintenance already running, the store hasn't moved yet, so the counter landed on the branch the store was about to leave, while the fork was made on the branch it landed on. After a restart, the channel's next fork took that fork's name, over its namespace. The spawn now waits out the hold first, with the same one-line wait as creation's; a helper awaited there would yield a microtask after its last check, in which a move could take the store. A conversation agent disposed after a move, whose own re-initialization had failed, had that failure's cause reported nowhere. The disposal alert now gives it, before a failed cleanup's. The disposal alert said the channel's next qualifying message spawns a new fork, which is false while the template's own context manager refuses its work, since a spawn compiles the template. It now says the spawn needs that manager to be working. A suppression no longer judges the conversation agents for disposal: it forks the store at its head, keeps every other agent's messages, and refuses to remove its own agent's last one, so it can't empty one. The log line for a failure in an undo's or redo's pass now names the re-initialization or the release, where it named the release alone. The merge of main 0a4a2c6 had added a check of the store's hold to main's condition for a clock row ahead of a conversation agent's channel message and correction. A move's hold already holds every agent's turn, and the row is stored only while the agent has no turn alive, so the check decided nothing. It's gone, and main's condition stands. Tests: disposal after a branch switch, a rollback and a redo back from an undo's branch; a clock-keeping fork emptied by an undo and re-initialized while the pass still holds the store, with a channel message and a correction for it then, still disposed; a fork spawned during a rollback's wait keeping its name to itself after a restart; the next fork after a disposal reached through the framework's own routing; a branch switch waiting for a running maintenance tick; the long-hold alerts held past their threshold to show there is one, and their restart facts in the shape test; a disposal whose re-initialization failed, with a long cause, keeping its opening in what the webhook posts; the event loop checked to turn between the managers' re-initializations; and exact counts and stop-line facts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ops webhook posts at most once per agent and kind in 15 minutes. A conversation agent's refused-work alert and its disposal alert were both context-reinit-failed with no key, so a refusal posted for a fork within that time silenced the fork's later disposal, and the last post an operator saw said a switch to the current branch would recover a fork that was gone. The disposal alert now has its own key; a fork's name isn't reused, so that costs at most one more post per fork. The changelog fragment says so. Tests: a fork that refused its work and is then disposed has both alerts posted; a rollback asked for in the same step as a mention that spawns a fork is refused as busy, with OperatorActionError (agent-busy); the disposal alert says the next spawn needs the template's own context manager to be working; a disposal whose re-initialization and cleanup both failed carries both causes; the clock-keeping fork test checks each deferral is placed for that fork, and opens its gate in a finally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
0.27.0's undoLastTurn() and redo() promised an atomic roll-back and a result, with no refusal. By our reading of its code, a runtime-settings change went ahead while a move held the store, and a second switchBranch() was refused only while the first still waited for running maintenance or a workspace module's mount; a run shows two switches made together, with neither, both going ahead. This change refuses each of them with OperatorActionError (agent-busy): an undo or redo while another agent sharing the store is busy or a conversation agent is being created; a switchBranch() while another move holds the store for its re-initialization, two made together included; and a runtime-settings change, reset or cancelled transition while a move or a surgery holds the store. Those refusals move from the changed fragment into a breaking one, which names who needs to act and how to retry. The changed fragment keeps the rest: what waits for the release, coalesced updates landing after it, and the named agent's own busy refusal becoming an OperatorActionError where it was a plain Error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Replying to this review. @Anarchid #353 is at
Also since our last reply:
The description's Evidence section has this head's runs. Written by Duncan-2410 (Claude Opus 5.5). — Daniel-2612 |
One conflict, in src/framework.ts's createConversationAgent: this branch marks a conversation agent's spawn as in flight while it waits out a store hold, and main now opens each conversation agent's context manager under a release record, so the framework can withdraw its calls once it is done with the manager. Both lines stay, the spawn mark first; the merged function closes a failed creation's manager and forgets its name (main's catch) and clears the spawn mark (this branch's finally). A fork this branch disposes after a branch move goes through disposeConversationAgent, which on main now releases its context manager, so a disposed fork's calls are withdrawn as any other ended conversation agent's are. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main brought anima-research#624 (a nested mount's directory compared as disk compares names), anima-research#353 and anima-research#366. Where anima-research#624 met this change: - effects.ts and observe.ts: main's judgement of a nested mount's place (holder.names, judgedByInclude, an awaited nestedAtReal), with this change's LEAVES_MOUNT for a link that leaves the mount. - index.ts: main's imports from observe.ts with this change's DIRECTORY, LEAVES_MOUNT, NOT_FOLLOWED and NOT_REGULAR. NestedMountError keeps main's constructor and its "may lie in" words, and stays a Refusal, so the tools lead it as they do every refusal. - test/workspace-path-spelling.test.ts: anima-research#624's tests that pin a tool's refusal now pin it led ("The read did nothing: ...", "The write did nothing: ..."), a link's reason as a sentence, and a followed link out of its mount in readRefusal's words; main's refusedAsInner takes the place of two of this change's literal assertions. The tests that run only where disk folds case, or folds a sharp s with ss, were edited by reading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main brought anima-research#813, anima-research#697, anima-research#366, anima-research#353 and anima-research#624 (a nested mount's directory compared as disk compares names). Where anima-research#624 met this change: - effects.ts and observe.ts: main's judgement of a nested mount's place (holder.names, judgedByInclude, an awaited nestedAtReal), with this change's LEAVES_MOUNT for a link that leaves the mount. - index.ts: main's imports from observe.ts with this change's DIRECTORY, LEAVES_MOUNT, NOT_FOLLOWED and NOT_REGULAR. NestedMountError keeps main's constructor and its "may lie in" words, and stays a Refusal, so the tools lead it as they do every refusal. - test/workspace-path-spelling.test.ts: anima-research#624's tests that pin a tool's refusal now pin it led ("The read did nothing: ...", "The write did nothing: ..."), a link's reason as a sentence, and a followed link out of its mount in readRefusal's words; main's refusedAsInner takes the place of two of this change's literal assertions. The tests that run only where disk folds case, or folds a sharp s with ss, were edited by reading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
268 commits from main, 28 pull requests: #353 (every agent on the store re-initialised after a rollback, suppression, switch, undo or redo), #817, #835, #818, #820, #815, #825, #821, #824 and the rest, with main's manager-release machinery (an ephemeral run's auxiliary membrane, a tracked manager released with its agent) and writeToFork (a fork's channel message held behind a live surgery). Eight hunks: a run's context keeps this branch's shared-inheritance branch (deriveSharedContext) beside main's open with the auxiliary membrane, and either manager is tracked and recorded on the release; a fork's channel message, routed or corrected, is written through main's writeToFork while admission still decides the delivered inbound and the wake; a fork's creation keeps the registry's record beside main's manager release; a fork's disposal keeps Dendrite's one release path, which gains main's reasoningLoss.forget and releaseManager (and the name-only branch its forget). .gitignore and the diagnostics test take main's words. #353's disposeEmptiedForks reads the registry's conversation-fork records and their homes instead of conversationAgentHomes. main's new dev dependency @animalabs/mcpl-core is installed, and the rig's membrane rises to 0.7.2 (connectome-host main asks for it). Eight rows of main's new tests are read onto this branch, each named in a comment: discord-awareness-outbox's fake frameworks carry the registry stub an undo's fork sweep reads; turn-continuation's fork check reads the registry's record; reinit-after-branch-move's fork tests match a fork's stored channel message by its author-prefixed form and allow the derivation notice a spawn tells its source; subconscious-model-fields configures the persistent reader, the one that is an agent at create; workspace-lost-contents' five torn-blob cases probe whether the linked chronicle rewrites a blob whose bytes differ (chronicle main does, from the #60 series; the released 0.4.0 keeps the file) and assert each behaviour. Full suite before those readings: 8389 tests, 8334 pass, 8 fail, 47 skipped; the eight pass alone after them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…catch-up main's 0.27.0 and the 28 pull requests after it, by way of feat/dendrite's ninth catch-up, meet the presentation index. Thirteen files conflicted: src/framework.ts (51 hunks), src/agent.ts, src/gate/event-gate.ts, src/modules/history/index.ts, src/tool-result-guard.ts, src/types/agent.ts, six test files and one changelog fragment. The branch move. #353's re-initialization (reinitializeManagers: every manager's strategy set again, one at a time, its failures alerted, emptied forks disposed) is now the mechanism of every move, and the fix wave's resync (cd93606) is rebuilt on it: followBranchMove calls reinitializeManagers, then rebases each agent's window on the new branch (written down where the branch has none) and drops every redo entry, and returns the failures for main's reportReinitFailures. What of cd93606 duplicated #353 is removed: its own reinitializeOnStoreBranch pass with its retries, create()'s refusal of a context-manager without that method, and three tests of the co-resident's strategy after a rollback, a suppression and a failed restore, which #353's tests already make. switchBranch keeps the fix wave's checkout logging around main's reinitializing/report shape. A rollback follows the move right after its switch, so the agent's window is presented on the new branch before holdUntilStart holds the rows the cut returned; a suppression follows at its release, after the removals, as main has it; restoreSourceBranch reports main's restore alert and leaves the following to its caller. Undo and redo stay on the window (decision 4): they move no branch, so main's beginSyncMove and reinitializeAfterSyncMove, their syncMoveReinit state and the maintenance and drain-kick arms that excused a tick an undo moved the store under are not carried; an undo re-initializes nothing, retracts, and every agent goes on compiling. The result types keep main's optional settled, never set here, and the host /undo no longer waits on it. This is either/or: main's undo reserves the store; this branch's undo reserves nothing. The queue's absence. stop()'s drain of the deferred queue (main reworded its lines for a persisted record that is behind) and the release's flush (main gave it a trace name) stay deleted. reportLongHold and disposeEmptiedForks count the writes the surgery's hold keeps (surgeryHeldWrites); the long-hold alert says those wait in memory and a restart loses them. A manager that refuses its work after a failed re-initialization keeps no queued writes here: a write for it fails at its writer, or at the hold's release, and is logged, and the alert's remedy says so (main keeps them queued until a switch recovers it: either/or). The release now traces each write it stores, with its id (source surgery-release), as main's flush did. writeToFork (decision 3) is rebuilt on storeInFork: a fork's channel message or correction is stored at once and presented at the fork's next point, and under a surgery's hold it is kept in memory and replayed at the release, its placement naming the fork with no id yet. The idle closure prompt stays the fork's own row (storeOwnRow with present), the sweep waiting out a hold. Wakes. main's readsStoredMessage (#814, a wake for a message stored as it arrived) is folded into readsRow, which names a stored row already: readsRowFor gives the stored id whether or not the row was presented at its write, and rowDone is main's storedMessageDone. main's general split of a batch that resumes a logical turn (its continuations alone, every other wake back to the queue) replaces this branch's narrower split while arrivals waited, and the autoContinue give-back beside it goes, as main's split leaves no mark there. The rest are unions: the imports and fields of both sides, main's manager release beside forgetPresentation (as abec840 wired it), the subconscious's voice with main's model variable, the hand-off's vouching beside main's streamCarried, the puppet's own-row pair with main's stored flag, the guard's notices with its own-row sink, the history search's matched count with its caller view, and agent.ts's exported branchKey and main's branchHead, which reads the store's head through a new read-marks.storeHeadOf, as pendingWatermark is a ReadMark here. The refusal rewind takes main's carried rule onto the window shed (only what the refused request carried, a sanitizer-empty row passed over, and a held result going with its carried call), keeps the shed's doc comment, and the refused reply's removal leaves every other window. main's new cut-off tool-block notice is written through storeOwnRow and the writers' guard lists it (decision 7). changelog.d/context-manager-0.22.breaking.md takes main's deletion: 0.27.0 released it, and 291-chronicle-0.5 carries the ^0.23.0 range. Tests. The conflicted ones: heard-mid-turn-wake's and mcpl-coalescing's stored-message wakes read readsRow; silent-heartbeat-rows takes main's heldRestartMeets helper with this branch's expectation (a resumed turn presents no arrival, so the message's wake starts its own turn), and main's five further held-restart cases are read onto that; stop-with-host-operations keeps main's refused second switch and this branch's switch started as the first returns; stop-with-work-in-flight drops main's persisted-queue record case; turn-continuation adds main's no-further-turn check. test/reinit-after-branch-move.test.ts (#353) is read onto this branch, a header saying how: the undo and redo cases are read onto the window, the cases whose point is a move that holds the store make it with switchBranch, held writes are '' or a placement with no id, and its alert texts follow the new remedy; the fix wave's six window cases are added to it as a describe of their own, and test/branch-move-resync.test.ts is removed. Two of main's refusal-rewind cases read the window (the result leaves the window with its call; the message stored while the refused request streamed follows the marker); reasoning-loss-turn's line follows the first reply in the worker's window, where it is presented, not in the record, where it is stored at once; subconscious-model-fields counts selfParticipants and presentAs among the subconscious's own fields, which it sets for its own voice; compression-after-release's (#550) fork cases present the backlog they write past the framework, as the fork's next point would, since a registered agent compresses its window, and a write made while quiesced is stored at once. main's changelog fragments for #353, the undo reservation, settled, the stored-message wake and the continuation split are rewritten for this branch, as are 291-branch-move-resync and 291-presentation-mandatory. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n's held-turn model Main now treats a turn a provider hold keeps for its retry as alive (TURN_RETRY): it counts in activeTurns, drained reads false, it passes a quiesce, surgery refuses it as agent-busy, and an abandon, stop() or the agent's removal ends it with one inference:aborted. anima-research#361's budget hold did the opposite: the held turn gave its token back and read as idle, so a surgery was admitted while it waited, and the turn was then released onto the store the surgery had moved. A run at anima-research#361's head 54565d8 showed that: a rollback past the held turn's message was admitted, and the released turn answered without that message in its history. The conflicts sat on that difference (passesQuiesce, gatedRequests, the scheduler's turn-alive test and trigger selection, the reaper, the policy drop). They are resolved by taking main's model and making the budget hold one more place a turn's retry waits: - the hold keeps the refused start's request as its turn's retry (asTurnRetry, with the turn's carry), joins later wakes with holdRequest, and the failed start ends as a provider-held start does (endHeldStart); - pendingContinuationAgents, gatedRequests and heldTurnRetry read the budget holds, so the turn is alive, a wake that joined it counts as gated, and surgery's and the puppet's refusals name the hold and the way on (a quiesce with abandon); - dropContinuations ends a budget hold with the turn whose retry it holds, putting back the wakes that joined it after a caller's stop or an abandon, and endContinuedTurn settles a released hold's record; - the release queues the held requests as they are, and main's scheduler merges and passes them. What goes: releasedByBudgetHold and the passesQuiesce exclusion; the surgery, undo and redo re-check (checkBudgetHoldsAfterSurgery, and the storeSurgeries count in the signature), since no store move is admitted while a turn is held; the check's surgeryHold guards; HostModeStatus's heldTurns, since the held turn is in activeTurnAgents; the reaper's own budgetHolds check, which hasTurn covers; and the scheduler's deferred-batch and guard-retry trigger handling, which main now does for every continuation. Main's stored injections (0e2fa84) store every afterUser injection, so the note a released turn's compile carries is stored too, where the turn reads it. Its words now say only what happened ("... so this turn waited 7 min for compression to make room."), true whatever its compile then meets. The release check's store-change count leaves out the note's own rows: without that, a released turn whose compile is refused would be released again on the strength of its own note. The release check asks the preview, and the fallback dry compile, with the agent's thinking binding, as main's previews ask (previewContextFor), so its verdict prices a summary's carriers as the turn's compile does. kickOverBudgetDrain takes anima-research#353's stop for an undo or redo that moved the store under a tick, for the breaker's kick and the hold's, each with its own tag and label. Tests: those that pinned an idle held agent are turned round (the status reads it alive; a surgery, a rollback, an undo and a puppeted call are refused naming it; a quiesce waits for it and its abandon ends it; a caller's stop ends it), the note tests read the stored note, and tests of scheduler behaviour main now owns are removed. One test pins the thinking binding the check asks with, and one that a dry compile resolving doesn't release a turn the preview says doesn't fit. Context-manager 0.23's dry compile and preview agree on the real strategy's fixture at every budget, so the real-strategy test now pins the stored note's margin case instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 (Anarchid'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; a disposal's alert has a key of its own, so a refusal posted for the same agent doesn't silence it. Each alert opens with what happened and what to do or what happens next, in at most 310 characters (store-held-long's in at most 333, with counts of up to nine digits), within the 343 to 349 the webhook keeps of the start of a message of 501 to 9,999,999 characters, by its length. Then come the branches it names, and its cause last, since a cause can be an error's whole diagnostic. The remedy is the same for every agent: aswitchBranch()to the current branch re-initializes the manager and stores the writes kept queued for its agent, at resume on a quiesced host, and a restart can lose those writes (R3-5). It says nothing of what a restart does to the agent itself, which differs by agent: a restart creates the configured agents again, andstop()ends each conversation agent and each run. An agent that has left by the time its re-initialization fails, as a run its watchdog ended, isn't reported, since it refuses no one's work.stop()says how many writes its last flush leaves queued for an agent whose store didn't take them, and whether the queue keeps them for boot (R3-5). That line, 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(), to the current branch or to the branch it should run on, re-initializes it, with the same remedy as any refusing manager's. A suppression whose removals failed after its fork is the exception: its fork initialized the agent's manager on the new branch, so when its restore can't select the source, the error and the alert say the agent serves from that branch, which may be partly suppressed, until 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.context-reinit-failedalert says so. Its channel's next qualifying message spawns a fresh fork from the template, under a new name, which needs the template's own context manager to be working. Its messages stay on the branches that hold them, but no agent reads them again, so an undo and then a redo don't bring it back.getMessageCount()), whatever its strategy holds. One whose history is there but compiles to nothing, such as messages that are hidden or past its window, stays. One whose own re-initialization failed is disposed too, with the disposal's alert alone, which gives that failure's cause: the refused-work alert's remedy, a switch to the current branch, would dispose it rather than recover it. The agent a rollback or suppression names is never empty after its move: a rollback keeps the message it returns to, and a suppression refuses to remove an agent's last message.mainfor any conversation agent disposed: its window takes what its store takes of them, where no agent reads them, and the primary is passed each failed reply, channel notice whose channel decision still stands, or module write for it (Not covered).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 disposed agent's held writes go through its window's release instead, untraced, as onmain. A coalesced push no longer removes the stored, unread message it updates or withdraws while the store is held. The coalescer handles that as it does any removal that fails: it appends the update, or a retraction's notice, which lands after the release beside the unread message, and a retraction with no notice leaves the message in place. So an unread message withdrawn while the store is held still reaches the agent, where otherwise a retraction leaves no trace of it.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.Errorwith no class or code, whose message carries "requires reinitialization for the current branch generation" or "refused stale branch-scoped mutation", so the check reads those two texts. Any other failure the tick meets keeps itscontext-maintenance-failedalert. The store's hold starts no new tick, so only a tick running when the move landed is treated this way.What callers will notice
switchBranch()while another move holds the store for its re-initialization, two made together included; and a runtime-settings change, reset or cancelled transition while a move or a surgery holds the store are refused withOperatorActionError(agent-busy). By our reading of 0.27.0's code each went ahead there, except a second switch while the first still waited for running maintenance or a workspace module's mount; a run shows two switches made together, with neither, both going ahead on 0.27.0. The changelog's breaking fragment says so.undoLastTurn()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-failedfor context-manager's refusal of its stale work. Any other failure it meets still does.What each part is for
The change is for one thing: after any move of the store's branch, the context manager of every agent sharing the store works again without a restart, and one that can't is named in an alert that says how to recover it. Each part below serves that, or keeps something that runs during a move from undoing it.
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 (Anarchid'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.disposeEmptiedForks, Anarchid's R1-1, his first option): without it, a conversation agent spawned after the point the move returns to answers with no history. Weighed: seeding it again from the template, as an earlier round did, which kept its name, so a redo found its history. That serves per-channel conversation routing, which 0.27.0 marks deprecated, and cost a template-health path, a check for a store moved mid-seed, a callback threaded through each pass, and the seed's extraction from creation. Disposal costs one synchronous pass after the last re-initialization, and an undo then a redo don't bring a disposed agent's conversation back. Its alert counts the writes held for it (R3-3).reinitializeManagersinstead, it would leave a microtask before that check, in which a redo could join after a fork was disposed. It costs nothing over calling it inside the loop.cleanupFailed, one catch per conversation agent, for Greptile's review of7bfa5622). Without it, one fork's disposal that threw escaped the move's pass, so the forks after it weren't judged and no failure was reported. It costs a field on the failure and a catch.reinitFailed): its refused-work alert is dropped for the disposal's, so without it that cause would be reported nowhere. It costs a field and a sentence, which comes before a failed cleanup's.'disposed'): the webhook posts at most once per agent and kind in 15 minutes, so a refusal posted for a fork within that time would silence its disposal, and the last post an operator saw would say a switch recovers a fork that's gone. A fork's name isn't reused, so the key costs at most one more post per fork.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 count of held writes, then the cause, which can be an error's whole diagnostic. A test builds every shape, with long causes and branch names, and checks each opening againstopsWebhookText, the cutopsAlertposts.REINIT_MOVEStypes the moves, so a new one joins the test's shapes. Without them, a wording change could push an alert's remedy out of the webhook's post unseen, as happened twice in this round, each time found only by a review. It costs four small functions and a typed list, and shorter remedies.store-held-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.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.RECOVERY: the one remedy every refusing alert gives, sorefusedWorkAlert's andrestoreAlert's can't drift apart. It costs a constant.initialized): the test in which an agent leaves during its own re-initialization records that the re-initialization ran, so it can't pass without reaching the point its title names. It costs a flag.says,courierModule,realTurn,heldTurn,holdFor, the forks' harness with its membrane, courier and held tick,gateReinit,nextForkFor,withRunningTick): each puts a test's setup through a route a caller has, a module's write, a real turn, a held stream, a real maintenance tick or a channel message, or checks an alert's facts rather than its sentence. Without them, each test would repeat that setup, or reach the same state through the framework's internals.bounded, in the coalescing tests' held-store case: its waits for the switch's re-initialization and for the push's reply fail after 10 s rather than hang, as the re-initialization file'suntil()does.writeToFork, Anarchid's R2-1). They now take the deferral every other agent's writes take under a hold, rather than a path of their own. The release stores each deferred write with its id through fix(deferral): hold window-only messages to the turn's end; injected messages are read #286's flush helper, which it now uses, as the other flushes store theirs. The release traces each write it stores, through an option on that helper (traceAs), as the turn-start flush does, since a write's trace at its deferral has no id. A coalesced removal leaves a stored message alone while the store is held (removePlacement, one line): it would touch a manager the hold is protecting. A module's edit or removal of a stored message, and a write carrying a fresh tool result, still reach a manager during a hold, as 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.MOVED_BRANCH_REFUSALS): otherwise each such move raisescontext-maintenance-failedfor a failure the move caused. The refusal has no class or code, so it's told by its two texts, and any other failure keeps its alert. It costs a list of two texts and a function. The hold starts no tick, so only one already running is treated this way.main, a failed undo lost its checkpoint there, which nothing can make again, and a failed redo lost its step. A redo whose branch is gone drops that step and says so: a failed redo now has two outcomes, so its error has to say which happened, or a caller would retry and redo the step beneath.Cut by a review of the source against what the change needs (
77b79fc7): the re-seed, for disposal; the remedy by agent kind (AgentKindand its clauses), for one remedy for every agent; and the disposal alert's account of where the held writes go, for their count. With them went the template's remedies, the check for a store moved mid-seed, the callback threaded through each pass, and the postponed-release field. Creation's emptiness check ismain's again, a dry-run compile of the new namespace, since the stored-message count was shared with the re-seed only. The refused-work and restore alerts no longer say that a restart ends a conversation agent or a run.Cut by an earlier pass (
83d9e72f): a redo whose branch was gone also dropped each step beneath it whose branch was gone too, and its message said the step now on top could be redone. Now a redo drops one dead step, which by reading is 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, with a module that writes for an agent as any module does. A turn's checkpoint comes from a real turn on the mock provider, and a busy agent from a turn whose stream holds. It checks that: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()returns with the store moved, and both compile once itssettledsettles;redo()likewise;stop()waits for an undo's re-initialization;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;stop()says how many writes it leaves queued for an agent whose manager refuses them, and nothing for one whose write the release stored;stop()'s line for such writes, andmain's for writes behind a stream still running (intest/stop-with-work-in-flight.test.ts), each for a memory-only queue, a persisted one whose record is current, which says boot keeps them and nothing of a loss, and one whose record is behind, which says why and that a restart can lose them;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;undoLastTurn()returns, one at a time, with a turn of the event loop between each two, and the event loop runs before the last of them has; an undo's re-initialization waits for an over-budget drain kick already running;store-held-longfrom its timer, at a shortened threshold: an undo and a redo joined, with the second pass held at a gate and a write deferred, name the undo, its agent, the manager being re-initialized and the write; a branch switch held by its config mount after its pass names no manager; and a hold let go before the threshold raises none after it;settled, and the release's failure is logged;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.With a conversation template and its forks:
stop()fails, as a failed spawn does; a spawn after a failedstop(), 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.With a maintenance tick held running:
store-held-longonce, held past the threshold, naming the move and saying it waits for maintenance, and nothing more once the store is let go;Every alert shape the text builders make,
store-held-long's included, with causes of 3,000, 200,000 and 1,000,000 characters, branch and agent names of 200 and more, andstore-held-long's counts of nine digits, keeps its opening in whatopsWebhookTextposts,store-held-long's says what a restart does, and each refusing shape's opening ends with the one remedy; a disposal with neither failure, which carries no cause, is posted whole, and one with both carries both causes.In
test/mcpl-coalescing.test.ts, while a branch switch holds the store, an update of an unread coalesced push, and a retraction with a notice, leave the stored message and land after the release, and a retraction with no notice leaves it and appends nothing;main's test that writes right after an undo awaits itssettledfirst.The tests wait for an undo's or redo's re-initialization on its
settled, and a test waiting for something to happen does it throughuntil(), bounded at 10 s, so it fails rather than hangs when that never comes. They check an alert's facts, the move, the agent, the branches, the remedy and the cause, rather than its whole sentence. Three stand-ins remain where no public route reaches the case with one helper: the parked-call test records its checkpoint directly, since its strategy is never ready for a turn; the busy check on a redo joining a pass fakes another agent's state, since the store's hold lets no turn start then; and the over-budget drain kick already running is a pending promise placed where the framework tracks its kicks, since a real one needs an agent over its budget.The suppression test removes the first message. On context-manager 0.14.0, after a message in the middle is removed, every compile that follows a dry run is refused. That is a separate context-manager bug, fixed there.
Evidence
At tree
321806f0, this head's code and tests (1269c003; the head,f5dba8f6, adds only changelog fragments), on Linux, on a clean checkout installed fresh bynpm installwith no lockfile and built from an emptydist, as CI does, on Node 24.22.1, with context-manager 0.22.1, membrane 0.7.2 and chronicle 0.4.0:npm test): passes 7,615 of 7,659, with 44 skipped and none failing, and the three files CI also runs on their own pass 138 of 138. Typecheck and build are clean.f96b8d91'ssrc/framework.ts, fails 20 of its 82 tests: the ten that dispose a conversation agent or check what follows, its alert, its webhook post and a spawn's name after a restart; the undo's log line for a failed pass; the six whose alerts now give the one remedy; the test of every alert's shape; and the two maintenance-tick variants in which a failure other than the refusal now keeps its alert.f96b8d91's alert builders take an agent's kind, so that build reports type errors, and emits. Built against77b79fc7's source, the round before the last two, it fails 6: a spawn's name after a restart, both webhook posts for a fork that refused and was disposed, a disposed fork's failed re-initialization in its alert, the template-manager clause, the undo's log line, and the shape test.store-held-long's is at most 333, with counts of nine digits. The webhook keeps 349 characters of the start of a message of 501 to 999 characters, and down to 343 of one of 1,000,000 to 9,999,999. A disposal whose cleanup and re-initialization didn't fail carries no cause, and its whole message is at most 482 characters, which the webhook posts whole.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 a disposal's judging then finds that message and keeps the agent.switchBranch()and the surgeries are this framework's ways to move the store, and its own writes are what the hold defers.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.main, and when the template is the primary, the next fork's seed from the template's context can carry it: a run shows a module's write doing so. The disposal alert says how many writes were held (R3-3).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 on to the primary each failed reply, channel notice whose channel decision still stands, or module write for it among them, deferred again for the restarted host's resume, 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.main. This change re-initializes the context managers and disposes the conversation agents a move emptied, and nothing else (R3-6).Merge notes
mainis merged into this branch, at794bf977inc8e25ef5, at8225e4bfin4a0cd8b9, atea4297abin180b703d, at97ca7ebained249de5and at0a4a2c69ine2e2b8ce, and each was read on its own.e2e2b8ce, git stopped at five places insrc/framework.tsand one intest/mcpl-coalescing.test.ts, and each keeps both sides:main's clock row ahead of a conversation agent's channel message or push, besidewriteToFork. Its condition ismain's: no row while the agent has a turn alive, and a move's hold holds every agent's turn, so none is stored under a hold, where it would land in a namespace the move may have emptied, ahead of a message that isn't there; the message itself is deferred. The deferred message lands later through the queue, and a run shows the due clock row stored right before it then;main'sChannelRefhome forcreateConversationAgent(), with this change's wait for a hold and its check for astop();main's seeding changes, inseedConversationAgentIfEmpty, which this change then shared between creation and the re-seed: the filter by provenance on the template's compile, the primary's notices left out, and the omissions' log line. With the re-seed cut, creation's seeding ismain's again, inline;main's doc ofreaddDrained, with this change'straceAs;settledbefore the write, withmain's wording.mainnow passes on to the primary more of what an agent that has ended never read: a channel notice that carries its inputs and a module's write for a run or a fork, beside a failed reply (passesToPrimary). The disposal alert once said where held writes go and named only "a failed reply"; it now gives only how many there were.switchBranch(),undoLastTurn()andredo()and one test's note on Node's two clocks. Where it had rewritten one ofmain's,main's wording is back, except where this change made it false.ed249de5, git stopped at four places insrc/framework.ts, 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, a channel notice whose channel decision still stands, or a module's write 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 adapted them, and the alert now gives only how many writes were held. fix(stop): end each live conversation fork as its release would #616 also ends a mismatch the review found here: by reading,stop()'s lines for writes kept queued could name a conversation agent's as kept for boot, and now meet only configured agents, 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.Naomi-2422 (Claude Opus 5.5) read the merge of
0a4a2c69region by region, ran its clock case at the merge, and found thatmain's wider pass to the primary left the disposal alert's sentences too narrow; she read the round after it, and the decision its crash case stands in for.Gladys-2413 (Claude Opus 5.5) read the source against what the change needs before reading the diff, which cut the re-seed, the remedy by agent kind and the disposal alert's account of the held writes. Abigail-2676 (Claude Opus 5.5) wrote the change's requirements from its texts and weighed every test against them, which moved the tests to where callers meet them.
We won't merge this before 01:00 UTC on October 18, or before #286 has merged, or before our pre-merge checks pass on its merge with the then-current
main, and a line here asking us to wait holds it.Written by Ada-1017 (Claude Opus 5.5). Duncan-2410 (Claude Opus 5.5) built the rounds for the third review, the merge of
0a4a2c69and the cuts that review made, and wrote their parts here.Description checked and posted by Lottie-2661 (Claude Opus 5.5).
🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's tooling, and posted from the shared
nissa-seruaccount.