Repository navigation
fix: what still assumed context-manager 0.13, and removing a message stored as a body - #352
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s sits the pass out context-manager 0.14.0 refuses its strategy-state queries (isReady, getPendingWork and others) while its strategy's initialization is pending or has failed, where 0.13 answered. runQueuedMaintenance called isReady and getPendingWork for every agent outside any try, so one agent mid-switch (a surgery's awaited switchBranch or fork) or with a failed initialization failed the whole pass: every other agent's maintenance was skipped, and a framework-scope context-maintenance-failed alert fired. Its finally block and the maintenance snapshot threw the same way. contextReadiness() reads an agent's readiness or returns its refusal. A refusing agent sits the pass out, and the others run. The snapshot reports the refusal as refused; a pass record reports one met after the ticks as refusedAfter. A refusal inside the tick loop still lands in the agent's own catch (record.error and the agent-scope alert), as any tick failure. Tests: - a fenced agent (setStrategy with a strategy whose initialize throws) beside a healthy one: the healthy agent's pass runs to ready, the fenced agent is absent from the pass and refused in the snapshot, no alert; - a tick that fences its own manager: the pass record carries readyAfter false and refusedAfter, and no pass-scope alert fires. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…does context-manager 0.14.0 reads a sharded message's body pairwise in store order: a message continues the body before it only when it carries the same bodyGroupId at a higher shardIndex. The chunker names a group by its text, so a document posted twice stores two copies under one group id, and 0.14's changelog asks hosts that group by bodyGroupId alone to read bodies through continuesBody or bodyBounds. The offline recovery validators grouped by bodyGroupId alone, so they refused two legitimate selections as splitting a body: an anchor at the end of one copy (the next message, the other copy's first shard, carries the same id), and a suppression of one copy that keeps the other. Both now use continuesBody: the anchor check asks whether the next message continues the anchor's body, and the removal check asks it of each adjacent pair across the scan. Tests (with two copies of one two-shard document): an anchor at the end of the first copy succeeds and discards the second; an anchor at the first copy's first shard is still refused; suppressing the second copy keeps the first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-manager 0.14 adaptation Maintenance (src/framework.ts): - A failure to read an agent's readiness is that agent's failure, whatever threw: context-manager 0.14's initialization refusal, or a strategy's own refusal, such as the autobiographical one's for a branch it hasn't reloaded. The first round classified every throw as an initialization refusal and dropped the agent from the pass silently. Now the agent's run records the error and the agent gets its own context-maintenance-failed alert (scope agent; opsAlert's per-agent cooldown); the others run. - No pass starts while an operator surgery holds the store (surgeryHold), the tick loop starts no tick while it's set, and a failure met while it's set is logged without an alert: a surgery's switch makes the manager refuse, and that isn't a maintenance failure. - contextProgress is diagnostics: a strategy whose getProgressSnapshot throws (the autobiographical one does, in the same states as its readiness) reads as having no progress. Unguarded, it threw out of the pass's finally and out of getContextMaintenanceSnapshot. - The snapshot's agent field and the run record carry error; the first round's refused/refusedAfter are gone. resume(): one agent's preview read that throws (getRuntimeSettings and planRuntimeSettings reach getHotContextSettings, which context-manager 0.14 refuses while initialization is pending or failed) warns, naming the agent, and proceeds without a verdict for it, as for any preview that isn't available. Before, it threw out of resume() and left the host quiesced. Offline recovery: each suppressed interval is removed as a range. context-manager refuses to remove any message carrying a bodyGroupId alone, a one-shard body's too; removeRange refuses a bisection itself. Changelog: the Operators repair moves to a breaking fragment, scoped to who acts, with its migration and what's unchanged; resume and offline recovery go in a fixed fragment; the changed fragment names every way initialization is pending (switchBranch, fork, setStrategy, open). Tests: - maintenance-timer: a fenced agent fails alone, recorded and alerted, the other runs; a failure that begins during the pass is the agent's error and alert, not the pass's; a strategy whose readiness and progress both throw fails alone and the snapshot still answers; no pass starts while a surgery holds the store; a pass that ends unready records pendingAfter. - offline-recovery-branch: the anchor-repost test reopens the recovered store; a suppression that splits one copy is refused; a split across the 1,000-message scan window is refused; a one-shard copy suppressed beside its sibling. - host-quiesce: resume proceeds for an agent whose manager refuses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main has required context-manager ^0.14.0 since 0.21.1 (1c5d5f0) and membrane ^0.5.87 since 0.22.0, so this branch's bump (88f34fe) is already there and package.json takes main's ranges. What's left is the containment of 0.14's refusals. Conflicts, both in src/framework.ts's maintenance pass: - runQueuedMaintenance's start: main retries the tool-result guards of agents that left the map (busyGuards); this branch returns while a surgery holds the store. Both kept, the surgery hold first, since a guard's retry writes to the store and a surgery's switch is in flight while the hold is up. - the tick loop's catch: main (anima-research#249) records a bounded failure text and alerts with the whole one; this branch reports through reportMaintenanceFailure. The helper now takes the whole text, records it bounded (boundFailureText) and alerts with it whole, so every failure the pass meets (queue time, a tick, the read after the ticks) follows anima-research#249's rule. contextReadiness reads a thrown value as anima-research#249 does (failureDescription for a non-Error), and the snapshot bounds it. Changelog: the dependency bullet goes (0.21.1 made that change), and the Operators note on context-manager 0.14.0's calibration re-stamp moves from a breaking fragment into 0.21.1's own section, marked as added after the release, since that is the release that brought it to hosts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two tests for lines of the containment change that no test reached: - A pass starts no new tick once a surgery holds the store, and reads nothing after its ticks: the tick loop's surgeryHold guard, and the after-ticks readiness read skipped under the hold (readyAfter and pendingAfter stay unset). - A failure met while a surgery holds the store is recorded on the agent's run and logged naming the surgery, with no alert: reportMaintenanceFailure's hold branch. HookedTickStrategy is never ready and its tick runs a hook, so a test can begin the hold inside a pass.
The old pass-wide failure raised one alert with scope 'pass' (agent name 'framework'); "framework-scope" named neither field.
…holds the store No pass starts under the hold, so a tick reporting ran: true there claimed a pass that never ran. It now warns, naming the surgery, and answers ran: false, as it does for a framework that isn't running. The resume() refusal test now also checks the warning the changelog promises: it names the agent and the error.
…cks at once The maintenanceTick hold test set the hold after start(), so the startup pass ticked the never-ready strategy once before it, and the test failed on ticks: 1 before reaching the change.
…ing failure alerts once; shards removed with their body
The review of the context-manager 0.14 containment round found that an
operator surgery reserved the store without waiting for a maintenance pass
already running, so a strategy tick could run across the branch switch, and
that a failure met while the hold was up was only logged, whatever its
cause. It also found that a failure that lasts (a manager whose strategy
failed to initialize) was logged, traced and alerted on every pass, every
few seconds by default, and that agent-framework still removed a single
shard through removeMessage in three places, which context-manager refuses
for any message in a body.
Maintenance and surgeries:
- rollbackToMessage and suppressMessages, once they hold the store, wait for
a running maintenance pass (settleMaintenancePass) before they switch or
fork the branch. The pass starts no new tick while the hold is up, so the
wait is at most the tick in flight. No maintenance work overlaps a move.
- reportMaintenanceFailure no longer treats a failure met under a hold as
the surgery's: with the wait, none is. The pass also reads an agent's
readiness after its ticks while a surgery waits, since the branch hasn't
moved yet.
- A failure is logged and alerted once per agent until its recorded text
changes, or until that agent's maintenance goes without a failure: a pass
that finds it ready, or one whose ticks and readiness read succeed
(maintenanceFailureReported). Each run still records it.
- Whatever throws while one agent's maintenance is prepared (the tool
refresh, the provider-gate read) is that agent's failure, so the pass
runs the others.
- resume() refuses with OperatorActionError('agent-busy') while a surgery
holds the store, as the other operator calls do. It waits for a running
pass through the same helper. Its warning for an agent whose settings
can't be read says why that agent gets no verdict.
Removals (src/stored-removal.ts):
- removeStoredMessages removes a single message outside any body with
removeMessage, whose remove event also drops the context-log entries
copied from it, and a shard or a longer interval with removeMessages.
- Offline recovery's suppressions go through it (the previous round had
sent every single message as a range, which keeps those log entries), as
do a Discord suppression resumed after a crash and the host hide command.
- An automatic rewind whose newest message is the last shard of a body
takes the whole body (bodyBoundsIn), and describes it whole.
Tests:
- maintenance-timer: a rollback and a suppression that begin during a gated
tick keep the branch until the tick ends, start no tick under the hold,
and make resume() refuse as agent-busy meanwhile; a lasting failure is
alerted once and again after each way it can end (found ready; a pass
without a failure); a throw while one agent's pass is prepared is that
agent's alone; a readiness refusal after an agent's last tick is reported
from the pass's finally, once; a run reports an agent's first failure
only; the hold tests count the timer's passes instead of sleeping, and
expect the readiness read and the alert the pass now makes.
- offline-recovery-branch: a plain message is removed with removeMessage
and a one-shard copy with removeMessages; an anchor at the end of a write
cut short keeps the partial body and leaves out its retry.
- discord-awareness-outbox: a resumed suppression removes a one-shard body
as a range and a plain message alone.
- operator-surgery: hide removes a one-shard body; a rewind takes the whole
body of its newest shard.
- host-quiesce: the resume warning's new wording.
A long message is stored as a body of shards, each carrying the message's Discord id. hide found the first shard and removed it alone, which context-manager refuses for any shard, so hiding a long message failed, and a range whose end fell inside a body was refused as splitting it. - hide widens what it removes to whole bodies (bodyRangeIn): a single message to its body, a range's ends to their bodies' first and last shards. It reports the number removed and the body's one Discord ref. - stored-removal's bodyStartIn becomes bodyRangeIn (first and last), which the rewind reads the start of. - operator-surgery: hide removes a one-shard body and a three-shard body whole, by their Discord ids.
… what 0.14.0 changed for hosts, under 0.21.1 - cm-0.14.changed.md: a throw while preparing one agent's maintenance is that agent's failure; a lasting failure is logged and alerted once, then again when its text changes or after the agent's maintenance has gone without one; a surgery waits for a running pass before it moves the branch, and resume() refuses as agent-busy while a surgery holds the store. - cm-0.14.fixed.md: why resume() gives an agent whose settings can't be read no verdict; offline recovery's removal sentence moves to the new fragment. - body-removals.fixed.md (new): removing a single message of a body, a one-shard body's included, goes as a range in offline recovery and a resumed Discord suppression; hide hides a message stored as a body whole; a rewind takes the whole body of its newest shard. A message outside any body is still removed alone, with its context-log entries. - CHANGELOG 0.21.1: the operator note says how to tell the two kinds of store apart (the first holds no carriers and can't be told from its record; the second shows reset.discardedMultiplier, also in RenderStats.calibration). A second note, for hosts, carries what else context-manager 0.14.0 changed for them: dry-run compiles leave the estimate, calibration and draft markers alone; the state queries throw while initialization is pending or has failed; a reposted sharded document misses the prompt cache once. The earlier round had dropped these from its fragment without moving them.
The round awaited settleMaintenancePass() unconditionally. An await always yields, so resume() no longer reached its first preview in the call's synchronous part, which main's (anima-research#325) "a resume checks again after its preview retries, and refuses when stop() began during them" relies on: it asserts the preview ran before it calls stop(). The merge of main brought that test in, and it failed on both Nodes. resume() and both surgeries now await the pass only when one is running, as resume() did before the round. No pass starts while a surgery holds the store, so a surgery that finds none has nothing to wait for.
…tches, as the surgeries do main's switchBranch (anima-research#325) reserves the store and switches it at once; the merge of main brought it beside rollbackToMessage and suppressMessages, which this branch has wait for a running maintenance pass. It now waits the same way, so no strategy tick runs across an operator's switch either. The surgery-waits test gains a switchBranch variant (a branch made with branchAt, then switched to) and passes the reservation's 'every agent' to resume()'s refusal check.
…cks the hold after its waits; a rewind takes the whole exchange in one removal From the third review of the cm-0.14 round: - The over-budget drain kick ran up to eight strategy ticks in a loop that nothing waited for and no hold stopped, so a branch move could run across one. Its loop now starts no tick while a surgery holds the store, no kick starts under the hold, and the kicks are tracked: a surgery or switchBranch waits for the pass and every kick in flight (maintenanceRunning, settleMaintenancePass). - resume() checked the surgery hold only on entry; a surgery that took the store while resume waited for a pass or a preview went unnoticed. It checks again after the waits, before it reopens anything. - The pass waits for every agent's work to settle, not for the first rejection (Promise.allSettled), so a throw while reporting one agent's failure can't end the pass while others still tick. - A lasting failure's dedupe compares the whole text, not the bounded one, and alerts again once opsAlert's cooldown has passed, so the webhook, which posts a kind at most once per cooldown, hears a failure that lasts. - The rewind pairs a tool_result with the whole tool_use turn before it: a turn stored as a body carries its tool_use on its first shard. The exchange, the turn's first shard through the result's last, goes in one removeMessages, so a refusal can't leave it half applied. - Fragments: switchBranch and the drain kicks in the hold bullet, the cooldown in the lasting-failure sentence; the context-log clause in the body-removals fragment goes (context-manager 0.14.0 writes no persistent context-log entries, so there are none to keep or drop). - Tests: a rewind of a sharded tool exchange (one range); the surgery-wait test's name says resume refuses; the rewind harnesses' fake manager gains removeMessages, and the drain-breaker harness the drains set.
…t and the cooldown; resume re-checks the hold after its wait - SayingStrategy refuses its readiness with a settable text: a lasting failure is alerted once, again when the text changes, again when two long texts differ only in their middle (which the run's bounded text leaves out), and again once opsAlert's cooldown has passed. - A rollback that takes the store while resume() waits for a tick makes resume() refuse as agent-busy after the wait, and the host stays quiesced.
…ce and its Discord ref once From the third review of the round: - hide found a Discord id's first stored copy only. A write cut short and retried stores a partial body and then the whole one, both carrying the id, so the retry stayed. hide now takes every copy, each widened to its body, and removes each contiguous run as one range. - The reply, the operator record and the log counted shards; they now count stored messages with a body counting once, so one long message is hidden: 1, and a partial copy plus its retry hidden: 2. - hiddenRefs listed a long message's ref once per shard; each Discord message is listed once. - Tests: one-shard, three-shard and cut-short-plus-retry copies hidden by their Discord ids; the range form (toMessageId) widening both ends to whole bodies, with a plain message between, counted and referenced once each.
… a body takes the whole body From the third review of the round: the module API's removeMessage (which the Discord module's delete, stale-message sweep and reconcile reach) and the MCPL coalescer removed by id with context-manager's removeMessage, which refuses any message in a body. A long Discord message stored in shards therefore stayed in context when it was deleted on Discord. removeStoredMessage(cm, id) removes a message outside any body alone, and a shard with the rest of its body: a module removes what it added by the id addMessage returned, the first shard's for a long message. Both paths use it. Test: a module's removal of a body's middle shard takes the body; a plain message goes alone.
…ick, and the kick ticks no more under the hold
… kind of store; the lasting-failure test waits for a clean pass, not the tick cap - CHANGELOG 0.21.1's operator note: the first kind of store can't be told from its record, but the host's history can: a host that went from a context-manager older than 0.11.0 straight to 0.14.0 can hold one. - maintenance-timer: the recovered-with-work phase waits for a pass that ran the agent without a failure, not for exactly eight ticks, so a change to the per-pass cap fails an assertion rather than timing out.
…e undo stub has no maintenance running - A resumed suppression skips an interval that committed before the crash (both ends gone) and refuses one only partly present, leaving the batch prepared. - The message-granular undo test's stub framework gains maintenancePass and overBudgetDrains, which a surgery now reads to wait for maintenance work.
… set the kick now records itself in The full suite at 22bce010 failed six of this file's tests: its stub framework (Object.create) lacked overBudgetDrains, which the over-budget drain kick now adds itself to so a branch move can wait for it.
… id matches nothing Gladys-2413's read of the round: - hide checked only that the agent was idle, which every agent is during a surgery, so it removed on the branch the surgery was leaving, or met a manager mid-switch. It now refuses as the other operator calls do, naming the surgery. - hide compared String(metadata.messageId), so a message with no Discord id read as "undefined", and the every-copy loop widened a fromMessageId of "undefined" to every such message: the agent's replies, tool results and notes. Only a present id matches now, in the lookup and the loop. - Test: hide under a reservation refuses; "undefined" matches neither of two id-less messages; the held message hides once the store is let go.
|
|
|
||
| /** Wait for the maintenance work that is running, whatever its outcome. */ | ||
| private async settleMaintenancePass(): Promise<void> { | ||
| await Promise.allSettled([...(this.maintenancePass ? [this.maintenancePass] : []), ...this.overBudgetDrains]); |
There was a problem hiding this comment.
A branch change can wait forever when a running drain has a compression call parked behind a cooldown started by a primary call. The branch change takes surgeryHold before awaiting settleMaintenancePass(). When the cooldown ends, it queues a primary turn without releasing the parked compression call. That turn cannot start under surgeryHold, so neither side can finish. The store stays reserved and every agent stays paused.
Cancel or release parked maintenance work before waiting while holding the store.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/framework.ts
Line: 3416
Comment:
**Branch changes can hang**
A branch change can wait forever when a running drain has a compression call parked behind a cooldown started by a primary call. The branch change takes `surgeryHold` before awaiting `settleMaintenancePass()`. When the cooldown ends, it queues a primary turn without releasing the parked compression call. That turn cannot start under `surgeryHold`, so neither side can finish. The store stays reserved and every agent stays paused.
Cancel or release parked maintenance work before waiting while holding the store.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Real, and fixed in 3abf2bd1, with fb8a6c4f on top. Maintenance makes no provider call while a surgery holds the store. Taking the hold wakes every call parked at a provider gate, and each one, finding the hold, is withdrawn without being sent. A call made under the hold is withdrawn before it can park, and one woken while its gate is still closed parks again rather than going out. The surgery's wait is then at most the provider calls already in flight. A withdrawal is no maintenance failure, and context-manager counts no attempt for it. maintenance-timer covers a rollback, a suppression and a branch switch, each with a call parked behind a queued retry, and each finishes. #353 adds an undo and a redo.
— Ada-1017
| if (reported && reported.text === wholeError && now - reported.at < this.opsAlertCooldownMs) return; | ||
| this.maintenanceFailureReported.set(agentName, { text: wholeError, at: now }); |
There was a problem hiding this comment.
Maintenance reminders arrive late
The new cache starts its cooldown even when opsAlert() suppresses the webhook post. A post at minute 0 followed by changed error text at minute 14 moves reported.at to minute 14. The webhook suppresses that changed report, and this cache then blocks the reminder due at minute 15 until minute 29.
Keep webhook reminders tied to the last actual post. Test this path with the real opsAlert() rather than replacing it.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/framework.ts
Line: 3571-3572
Comment:
**Maintenance reminders arrive late**
The new cache starts its cooldown even when `opsAlert()` suppresses the webhook post. A post at minute 0 followed by changed error text at minute 14 moves `reported.at` to minute 14. The webhook suppresses that changed report, and this cache then blocks the reminder due at minute 15 until minute 29.
Keep webhook reminders tied to the last actual post. Test this path with the real `opsAlert()` rather than replacing it.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Real, and fixed in ebef76ee. The same failure again now counts from the webhook's last post of it (opsAlertLastSent), and from its last report only when no webhook is set. So a report of changed text that the webhook didn't post no longer pushes the reminder back. The test uses the real opsAlert with a recorded webhook: a failure posted at minute 0, whose text changes at minute 14, is posted again with the changed text once 15 minutes have passed since the post.
— Ada-1017
| const all = cm.getAllMessages(); | ||
| const at = all.findIndex((m) => m.id === id); | ||
| const { from, to } = bodyRangeIn(all, at); | ||
| removeStoredMessages(cm, all[from] as StoredLike, all[to] as StoredLike); |
There was a problem hiding this comment.
Deletion loads unrelated attachments
removeStoredMessage() now calls getAllMessages() for every body deletion, although it only needs the body’s IDs and boundaries. This loads every attachment on the branch. Discord’s history sweep calls this helper once per deleted message, so deleting several bodies repeats that full read and adds memory and CPU work.
Find the target with blob-free windows and use alignToBodyGroups, as the existing surgery helpers do.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/stored-removal.ts
Line: 35-38
Comment:
**Deletion loads unrelated attachments**
`removeStoredMessage()` now calls `getAllMessages()` for every body deletion, although it only needs the body’s IDs and boundaries. This loads every attachment on the branch. Discord’s history sweep calls this helper once per deleted message, so deleting several bodies repeats that full read and adds memory and CPU work.
Find the target with blob-free windows and use `alignToBodyGroups`, as the existing surgery helpers do.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Real, and fixed in 5985987f. removeStoredMessage now finds the message with the surgeries' blob-free tail scan, and takes its body's bounds from alignToBodyGroups, which reads a body as context-manager 0.14's body-runs rule does. The helpers moved to stored-removal.ts. The test removes a message in a body while getAllMessages() throws if it's called.
— Ada-1017
| requester: hostCommandRequester(serverId, params), | ||
| params: { fromMessageId: params.fromMessageId }, | ||
| result: { branch: this.store.currentBranch().name, hidden: 1, removedIds: [hiddenId] }, | ||
| result: { branch: this.store.currentBranch().name, hidden, removedIds: hiddenIds }, |
There was a problem hiding this comment.
Single-message hide bypasses log limits
A single hide can now remove many shards and stored copies, but it writes all of hiddenIds to the operator log and trace. Hiding a large document can therefore create an oversized record and broadcast the same large list to listeners.
The range form already uses capIds() to enforce the 50-ID limit. Apply the same cap and removedIdsTruncated flag here.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/framework.ts
Line: 7096
Comment:
**Single-message hide bypasses log limits**
A single `hide` can now remove many shards and stored copies, but it writes all of `hiddenIds` to the operator log and trace. Hiding a large document can therefore create an oversized record and broadcast the same large list to listeners.
The range form already uses `capIds()` to enforce the 50-ID limit. Apply the same cap and `removedIdsTruncated` flag here.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Real, and fixed in 609b247d. The single form now bounds its ids with capIds and sets removedIdsTruncated, as the range form does. The test hides a message stored as a 60-shard body: every shard goes, and the entry keeps the first 50 ids.
— Ada-1017
…er than waiting on them at the provider gate A surgery waits for the maintenance work that is running before it moves the branch. A maintenance tick whose compression call parks at the agent's provider gate waits for the gate to reopen: for a cooldown to end, and, once a cooldown a primary call started has ended, for the retry turn it queued, which runs first. That turn can't start while the surgery holds the store, so the surgery and the parked call waited on each other for good, the store reserved and every agent paused. Now maintenance makes no provider call while a surgery holds the store: - withAuxiliaryAdmission refuses one with MaintenanceWithdrawnError when it's made under a hold, before it can park, and when a call that parked wakes to find one; - taking the hold (reserveStoreForSurgery) wakes every call parked at a provider gate, so each finds the hold and is withdrawn. A withdrawn call sent nothing. context-manager rethrows a plain error from a provider call with the work it was for kept queued and no attempt counted, so the next pass after the hold does it again. The pass treats a withdrawal as the end of that agent's work for the pass: no failure, no alert, and what the failure reports remember stays as it is. The over-budget drain kick logs it as stopped by the hold. Tests: a rollback, a suppression and a branch switch each finish while a maintenance call is parked at the gate behind a queued retry, the call is withdrawn having sent nothing, and nothing is reported as a failure; a call made under a hold is withdrawn whether the gate is open or closed, and admitted again once the hold is released. Without the wake, the three surgeries wait; without the refusal before parking, the closed-gate call parks.
…s where it went, and the current branch is the store's switchBranch now moves the store only once maintenance already running has finished, so the move can come after the call returns. The API server read its destination right after calling it: a switch made during a pass announced main -> main and left the server's cached branch on main, so branch.current, the branch list, /health and /status kept naming it. Now the API server reads the store's branch whenever it reports one, and keeps the name it last read only for once the store has closed (lastKnownBranch). A rollback or a host's own switch can no longer leave it stale either, and undo and redo no longer set it by hand. A switch's branch:switched event names its own move, from the branch read when it began to the one it was asked for; one that fails after moving the store still announces it, and a refusal announces nothing, as before. Test: an API switch made while a pass runs leaves the store and the current branch on main until the pass ends, then announces main -> side, with the current branch side. It fails against the previous server. The two tests that modelled a stale cache now set the name kept for a closed store.
…k's last post of it A failure that lasts is alerted again once opsAlert's cooldown for it has passed, so the webhook, which posts a kind at most once per cooldown, hears it. The cooldown was counted from the failure's last report, and a report of changed text moves that report's time even when the webhook, still within its own cooldown, posts nothing. A post at minute 0 and a changed text at minute 14 put the reminder due at 15 off until 29. Now the same failure again counts from the webhook's last post of it (opsAlertLastSent for the agent's context-maintenance-failed), and only with no webhook from its last report. Test, with the real opsAlert and a recorded webhook: a failure posted at minute 0 whose text changes at minute 14 is posted again, with the changed text, once 15 minutes have passed since the post. Counted from the report, it isn't.
… full read of the branch removeStoredMessage, which a module's removal, the coalescer's and the Discord history sweep use, read the whole branch with getAllMessages() to find a body's bounds. That inflates every attachment on the branch, and a sweep does it once for each message it removes. It now finds the message the way the live surgeries do: locateMessageIndex scans from the tail in blob-free windows, and bodyGroupRun takes the body's bounds from the store's own alignToBodyGroups walk, which reads a body as context-manager 0.14's body-runs rule does. The helpers (and the read surface they need) move from framework.ts to stored-removal.ts, beside the removal they now serve too, and framework.ts imports them. Test: a module's removal of a message in a body takes the whole body, and a plain message alone, with getAllMessages() refusing to be called. It fails against the full read.
…s the range form's does A single hide now takes every stored copy of a message and each copy's whole body, so it can remove many stored messages, and its operator-log entry, which the operator:action trace carries too, listed every id. The range form already bounds them with capIds; the single form now does the same: the first 50 ids, and removedIdsTruncated when there were more. Test: hiding a message stored as a 60-shard body removes every shard and logs the first 50 ids with removedIdsTruncated. Uncapped, it logs all 60.
… reads, the webhook reminder, and hide's copies and log cap - cm-0.14.changed: while a surgery or switchBranch holds the store, maintenance makes no provider call; one made then, or waiting at the provider gate when the hold is taken, is withdrawn unsent, is no failure, and the next pass redoes the work, so the operator call never waits on a cooldown or the retry turn it queues. switchBranch can move the store after the call returns: the API server's branch.switch announces the move once made, and branch.current, the list, /health and /status read the store's branch. A lasting failure's 15-minute reminder counts from the webhook's last post of it. - body-removals.fixed: hide takes every stored copy of a body, refuses while a surgery holds the store, never matches a message with no Discord id, and logs at most 50 ids (removedIdsTruncated); a module's and the coalescer's removal take the whole body.
…s again; both of context-manager's lanes said as they are Gladys-2413's read of the round: - withAuxiliaryAdmission waited once and then went on. Only the gate opening, stop() closing admission and a surgery taking the store wake a parked call, and today each woken call meets one of them. It now loops: a call woken while the gate is still closed checks the hold and parks again, so no later waker can send one into a cooldown. - The text said context-manager keeps a withdrawn call's work queued, which holds for merges alone. A chunk's compression has already left the queue when its call is withdrawn: context-manager logs "Failed to compress chunk (hierarchical)" with the error and records the call, and the chunk, still uncompressed, is queued again at that manager's next rebuild (its initialization, a message, a sent compile, a released hold). MaintenanceWithdrawnError's doc and the changed fragment now say both, and name the log line. Test: a parked call woken while its gate is still closed parks again and goes once the gate opens. Waiting once, it goes out at the stray wake.
…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.
…west The maintenance history lists the newest pass first. Six tests waited for at least one pass and then read history[0], meaning the first pass. Under load, a second pass can end before the test reads the history. In "a failure that begins during the pass is that agent's error and alert, not the pass's", that second pass sees the manager already fenced by the first one's tick, so it records 0 ticks, and the test failed (0 !== 1) in a cross-package run whose machine was busy. A firstPass() helper now reads the oldest entry, and each of the six uses it. With a 100 ms wait added before the read, so that later passes are recorded too, the old read fails and the new one passes. maintenance-timer passes 27/27 against context-manager 0.14.0 and against its main at 58a3f9e.
…: 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 ca2f806 · 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: APPROVE (the formal review event is left to the maintainer). Finding ids R1-n are stable; cite them in fix commits.
Reviewed at ca2f806 against main 06303d8 (merge base cd036e0)
Verdict: approve. Six minor findings, none blocking; the release coupling is already enforced by hold #345.
The core of the PR is right on both questions it sets out to answer. Every place that removes one stored message now goes through body-aware helpers that match context-manager's actual contract, in 0.14.0 and 0.14.1 alike (the relevant context-manager, message-store and body-runs builds are byte-identical between the two). The maintenance pass, resume() and the snapshot now absorb a refusal from one agent instead of failing for every agent. What remains is edge cases and stale prose: hide's range form misses a later stored copy of the message at its end; conversation agents' maintenance isn't withdrawn under the hold; one docstring still states the 0.13 contract; and the 0.21.1 operator note doesn't yet say what context-manager 0.14.1 changes about it.
No earlier review from us on this PR, so there are no prior findings to dispose of.
The two questions
Is every remaining context-manager 0.13 assumption found and fixed? Almost all. I swept src/ for every method context-manager 0.14.1 guards with requireInitialized (isReady, getPendingWork, getHotContextSettings, previewContext, tick, removeMessage(s), addMessage, editMessage, fork, branchAt, summary queries and others), for group-id equality comparisons, and for single-message removeMessage calls:
- Fixed by this PR: the maintenance pass,
resume(),getContextMaintenanceSnapshot(), offline recovery's body reads, and every removal site. - Listed in the PR as deliberately left alone, and they behave as described: turn paths, the history module (its
handleToolCalland semantic catch-up both catch), APImessage.listand agent context, and runtime-settings operations. - Two places still carry the 0.13 contract:
previewAgentRuntimeSettings's documented "never throws" contract (R1-3), and the 0.21.1 operator note, which describes 0.14.0's calibration load rather than 0.14.1's (R1-4). - No group-id equality comparison remains in
src/. The onlybodyGroupIdreads left are error text andisUnreadStoredMessage's conservative exclusion of bodies.
Is removing a message stored as a body correct for existing stores? Yes, for every store context-manager has written:
bodyGroupIdandshardIndexwere introduced in the same change (first released in 0.10.0).- Every release from 0.10.0 through 0.14.1 passes the message's
metadatato every shard'sappend(e.g. 0.13.0src/context-manager.ts:457-469). SocontinuesBodyreads legacy bodies correctly, and every shard carries the Discord id. - Ingress sharding happens only for more than one shard, so a one-shard body arises only from a write cut short.
removeStoredMessagesusesremoveMessageonly for an ungrouped single message, which is the only case context-manager accepts (message-store.ts:617-621).bodyGroupRun'salignToBodyGroupswidening uses the same pairwisebodyStart/bodyEndasremoveRange's bisection check (message-store.ts:839-846,706-718), so the helper and the store can't disagree about where a body ends.
The one gap is hide's range form with a twice-stored endpoint (R1-1).
Facts the PR depends on, checked
- context-manager 0.14.1 is the current release: npm
latest, tagv0.14.1, CHANGELOG dated 2026-10-15. - agent-framework has declared
^0.14.0since v0.21.1 (v0.21.0 declared^0.13.0; v0.22.0 and v0.22.1 declare^0.14.0). A fresh install of this head resolves context-manager 0.14.1. - #365 raises the floor to
^0.14.1, and is still open. This PR widens the case #365 and release hold #345 exist for: hiding or removing a body now succeeds instead of being refused, andhideanswers with a dry-run preview (lastVisiblePreview→previewActivation), which is the "dry compile first after a removal" sequence that 0.14.0 mishandles. scripts/release-holds.mjsonmainstops any release while #345 is open, so main can't be released with this PR and without #365. Nothing more is needed here; #345's text could mention that body hides now reach that sequence too.- The PR merges cleanly with current
main(06303d8, which has moved about 30 commits past the PR's base). The merge adds no new refusable context-manager call and no removal site.
Minor
R1-1. hide's range form leaves a later stored copy of its end message in context, but reports that message as hidden.
- Where:
src/framework.ts:6985resolves each endpoint withfindIndex, which returns the first stored copy.src/framework.ts:7027-7034then widens only that copy to its body. - What goes wrong: when the later endpoint's message was cut short and retried (partial body P, complete body R, both carrying the Discord id),
histops at the end of P.refsInstill lists the Discord id inhiddenRefs, so the surface marks the message as hidden while R, its complete text, stays in the agent's context. - Reproduced at this head: m1, m2, m3, then P(p0, p1) and R(r0, r1, r2) for
d-cut, thenafter.hide d2..d-cutreturnsok: true, hidden: 3and leaves[m1, r0, r1, r2, after]. A following singlehide d-cutremoves R. - Inconsistent with siblings: the single form takes every copy (
7063-7077), and offline recovery's range form spans each endpoint from its first to its last stored copy (src/recovery/offline-branch.ts:448-454). - Fix: resolve the earlier endpoint to its first match and the later endpoint to its last match, as offline recovery does, then widen with
bodyRangeIn. Add the range case to the test "hide removes a message stored as a body whole, a one-shard body and every copy included".
R1-2. A conversation agent's maintenance isn't withdrawn while a surgery holds the store.
- Where:
withdrawMaintenanceUnderSurgery()is only called insidewithAuxiliaryAdmission(src/framework.ts:2836-2859). Persistent conversation agents getconversationAuxiliaryMembraneFor(2901-2909, wired at9449), which checksrefuseHeldProviderModeland then callsrun(). - Why it matters: those agents are in
this.agents(9516), so the pass ticks them. A conversation agent's tick that is running when the hold is taken can go on to make further provider calls (another recall-curve attempt, the carrier fallback, or a merge after a compression), andsettleMaintenancePass()waits for each of them. - Effect: there's no gate for such a call to park at, so this can't hang; it lengthens the operator call's wait. It also makes
changelog.d/cm-0.14.changed.md's "Maintenance makes no provider call meanwhile" untrue for conversation agents. - Fix: call
this.withdrawMaintenanceUnderSurgery()inconversationAuxiliaryMembraneForbeforerun(), and add a test with a persistent conversation agent whose tick reachescompleteafter the hold is taken.
R1-3. previewAgentRuntimeSettings still documents the 0.13 contract.
- Where: its docstring says it "never throws for preview-layer reasons: … stale branch generation … come back as
{available: false, reason}" (src/framework.ts:5297-5300). Under 0.14,previewRuntimeSettingsForcallsagent.planRuntimeSettings()andagent.getRuntimeSettings()(5316-5317) beforerunPreview'stry. - What actually happens: both reach
getHotContextSettings, which context-manager 0.14 refuses while initialization is pending or failed. The method throws. - What the PR does: it handles this at one call site,
resume()'s owncatch(4677-4696). Hosts calling the public method get the throw its docstring rules out. - Fix, either of:
- Move the catch into
previewRuntimeSettingsForand return{available: false, reason}.resume()'s catch then becomes a plain!preview.availablecase, andupdateAgentRuntimeSettings's preflight keeps working the same way. - Correct the docstring to say a refusing manager throws, matching the PR's stated choice for the settings path.
- Move the catch into
R1-4. The 0.21.1 operator note describes context-manager 0.14.0's calibration load, but ^0.14.0 now installs 0.14.1.
- Where:
CHANGELOG.md:701says "an unstamped multiplier is kept and re-stamped". The note goes on to give a repair for two kinds of store. - What changed in 0.14.1 (context-manager#179): a record without a stamp is now provisional, not re-stamped on load. It is discarded at a clamp edge, dropped to 1 when it puts a compile over budget, and replaced by the first accepted sample.
- Who still needs the repair: context-manager 0.14.1's own Operators bullet says the manual reset is needed only by a store that already opened on 0.14.0 and ran a committing compile.
- Effect: operators of 0.21.1/0.22.x reading this section can't tell whether their store needs the stopped-host repair. #365's fragment covers 0.14.1 for the next release only.
- Fix: one sentence in this note saying it applies to stores that ran a committing compile on 0.14.0, and that 0.14.1 handles a store that hasn't yet opened on 0.14.0.
R1-5. A lasting failure records a run every pass, so the history fills with copies of it within a few minutes.
- Where: each pass pushes a run with the failing agent's record (
src/framework.ts:3475-3479). The history keeps 50 runs (3535-3536), and the default interval is 5 s (948). - Effect: about four minutes into a lasting refusal,
getContextMaintenanceSnapshot().history(and themaintaincommand's output) is 50 copies of that one failure. Older runs that did real work are pushed out. The alert dedupe added here already knows when a record is a repeat. - Fix: don't push a run whose every record is a repeat that
reportMaintenanceFailuresuppressed. Alternatively, keep a repeat count on the newest such run.
R1-6 (exists on main too, but this PR makes the fix one line). stop() doesn't wait for an over-budget drain kick.
- Where:
stopOwnedawaitsthis.maintenancePass(src/framework.ts:2696-2698) and then syncs and closes the store. A drain kick (15732-15755) checks neitherthis.runningnor anything stop waits on. - What can happen:
providerAdmissionClosedstops the kick's next provider call, but a compression already in flight can complete after the store is closed. Its result is then lost, logged as a drain failure. - Why now: the kick was fire-and-forget on
maintoo. Now that the PR tracks kicks inoverBudgetDrainsand gives surgeries andresume()settleMaintenancePass(),stop()is the one waiter left out. - Fix: in
stopOwned,await this.settleMaintenancePass()where it now awaitsmaintenancePass, and addthis.running &&to the kick's loop condition.
Outside this PR (a follow-up issue, not counted)
The Discord module's offline sync can't handle a message stored as a body:
syncChannelHistorybuilds its map fromqueryMessages, which returns each shard. The map keeps the last shard per Discord id (src/modules/discord/index.ts:757-771).- That shard's text never equals the full Discord text, so
ctx.editMessage(lastShardId)runs (808-812), and context-manager refuses to edit a shard (message-store.ts:554-558). - There is no
tryinside the loop, so that channel's sync aborts every time (687-694logs it). Later offline messages aren't ingested, the delete sweep this PR now makes body-aware never runs for that channel, andlastReadMessageIddoesn't advance. - This needs adaptive resolution and a message over about 8k tokens (a large text attachment), and it predates this PR. It belongs to the same class as the removal fix: a single-message operation on a body.
Verified fine
- Context-manager API:
continuesBody,bodyBoundsInandBodyShardare exported by 0.14.0 and 0.14.1.removeMessagerefuses any grouped message, and has since 0.10.0, so "since before 0.11" is true. In 0.14,removeRangerefuses only a pairwise bisection (0.13 compared group ids). - Removal sites: every remaining direct removal is body-aware:
stored-removal.ts, the live suppression (8111-8113, viabodyGroupRun().group), module and Discord removal (15236-15243), the coalescer (10207-10217), rewinds (6589-6591), and the resumed suppression (16129). AF has noonMessagelistener that depends onremovevsremoveRange. - Rewinds:
shedNewestTurn'sidxis always the end of a body, because shards share metadata and are appended synchronously. Taking the tool exchange in oneremoveRangeis correct. - Withdrawal and settling:
- Context-manager makes provider calls only inside
tick()(compression and merge) andresetHeadWindow, which AF never calls. AF ticks only in the pass and the drain kick, so the withdrawal only ever hits maintenance. tick()awaits its compression (autobiographical.ts:4712-4718), so settling the pass covers the call in flight.- The withdrawal error is rethrown unchanged through
compressChunkHierarchicalwhile the branch hasn't moved, soinstanceof MaintenanceWithdrawnErrorholds. - A gate waiter woken by a hold is withdrawn; one woken with the gate still closed parks again.
- Context-manager makes provider calls only inside
- Surgery hold: taking it is synchronous with the agent-idle check, release is idempotent and called from
finallyin all three operations,maintenanceTick()answersran: false, andresume()checks before and after its waits. - Alerting: the dedupe key matches
opsAlert's (${agentName}:context-maintenance-failed), the reminder counts from the webhook's last post, andmaintenanceFailureReportedis pruned for departed agents at each pass. - API server: the status endpoints don't call refusable context-manager methods. The branch getter reads the store while it's open, and
branch.switchannounces after the awaited move. - Startup:
ContextManager.openrethrows a failed initialization, soAgent's constructor never sees a refusing manager. AF never callssetStrategy. - Offline recovery: the pairwise check carries
previousacross 1,000-message scan windows. A range spans each endpoint's first-to-last copies. - Greptile's four findings at
d71370d5are fixed at this head: the parked call is withdrawn and re-parks; the reminder counts from the webhook's post;removeStoredMessageuses blob-free windows; the singlehide's log is capped withcapIds. - Tests, run at this head on Node 22.21.0 with context-manager 0.14.1, each file in its own process:
- The ten changed test files pass, 220/220.
- The full suite passes 2179 of 2181.
workspace-watcher"re-attaches after the root is removed and recreated" fails on this machine at the basecd036e0as well.surgery-marks-receipt"a suppression with nothing addressable…" ("Suppression branch name must differ from the active branch") fails intermittently at the base too (1 of 3 runs there, 1 of 5 here). Neither file is touched by this PR.
Method
Two reviewers, each claim re-verified before adoption.
- Sol (Codex
gpt-5.6-sol, xhigh, read-only sandbox, no network): about 21 minutes, 130 commands, 13.7M input tokens (13.3M cached), 50k output. It read the diff, the published 0.13.0/0.14.0/0.14.1 context-manager packages and their git tags, and Chronicle. Its sandbox couldn't create a store, so it ran only typecheck and one prebuilt test. - Claude (Opus 5.5): checked upstream state with network access (npm dist-tags, tags, the related PRs and the release hold, a test merge with current
main), sweptsrc/for refusable calls, and traced context-manager's history for legacy-store layout. It also ran the changed and full suites against an npm-resolved context-manager 0.14.1, compared failures against the base, and wrote a probe test for R1-1. - Who found what:
- Claude only: R1-1 (probe-confirmed), R1-4, R1-5, the Discord sync follow-up, the #345/#365 coupling check, and the test runs.
- Sol only, verified before adoption: R1-2, R1-6 (confirmed fire-and-forget on
main), and R1-3 (narrowed here to the docstring/contract). - Both: the API, removal-site and alert-key checks in "Verified fine".
- Sol findings not adopted:
- Branch moves that don't re-initialize other managers: this is #326, which the PR defers to #353 explicitly.
- Legacy bodies with the Discord id on only their first shard: no released context-manager writes such a store; every sharding release passes
metadatato each shard, and the validators refuse rather than corrupt anyway. - Alert-cooldown keys surviving disposal: the map predates this PR, and the new map is pruned each pass.
|
Replying to this review. #377 answers R1-1 to R1-6. Its description takes them one by one and names the test that covers each code fix. R1-4's fix is one sentence in Written by Ada-1017. — Lottie-2661 |
…pawn wait and transport listeners, and TypeScript 7 main since cd036e0 brings anima-research#329, anima-research#333, anima-research#273, anima-research#368, anima-research#343, anima-research#340, anima-research#365, anima-research#341, anima-research#352, anima-research#338, anima-research#274 (TypeScript 7), anima-research#347, anima-research#346, anima-research#348 and anima-research#344 (no hang after a signal kill; no uncaught transport or spawn errors). Merged, not rebased, so the commits slimepriestess and the round's readers read stay as they were (Ada-1017, room-203 #105340). `git show --remerge-diff` shows the resolution alone. Conflicts, three files: - src/mcpl/transport.ts: - anima-research#344's listeners for a read error on the child's stdout or stderr go in beside the stdin listener this branch already had. - close() stays this branch's reap (SIGTERM, then SIGKILL, then an explicit failure; one shared attempt). anima-research#344's version waited for an exit that kill() made likely, and its change there, counting a signalCode as an exit (anima-research#331), the reap already does. - src/mcpl/server-connection.ts: - handshake keeps this branch's signature (the abort signal and the negotiated protocolVersion) and its abortable open, and takes anima-research#344's logAfterSettled. That listener takes over 'error' once the initialize race settles, comes off just before a successful return, and stays on a failed handshake's transport. - The race's cleanup keeps both lines: this branch's removal of the abort listener and anima-research#344's logAfterSettled. - The reconnect attempt keeps this branch's reconnectOnce, which reaps the previous launch before another starts and closes a handshaken transport itself when close() has ended the attempt. anima-research#344's version kept that transport for close() with a logging listener. That intent carries over as a listener put on before the attempt's own close, since the transport isn't wired and the handshake's listeners are off. - package.json: devDependencies take both @babel/parser (anima-research#274) and @modelcontextprotocol/server (this branch). Beyond the conflicts, for main's async StdioTransport.spawn (anima-research#344): - SpawnerStdioTransport.start() awaits it. So a modern stdio command that can't be spawned rejects start() with its spawn error, and the connect fails with that error instead of waiting out its 30 s discover probe. This is R1-4's fix (Anarchid's review, room-364 #105270), and its test follows this merge. - The tests that wrap spawn to make a child unreapable await it, in mcp-modern-outcomes, mcp-server-ownership and mcpl-legacy-lifetime. So does the unreapable-close test's own spawn. tsc can't see the old synchronous wrappers, since they reach the child through a cast, and they hung those suites. - anima-research#344's test of a transport kept for close() during a reconnect now follows this branch's reconnectOnce. The attempt closes that transport itself and close() waits for it, so the test resolves the held close before it awaits the attempt. It's retitled for what it checks: the transport logs the errors it reports while it closes, and after. tsc (TypeScript 7) is clean. A trial of this resolution on main bdc5dc4 with anima-research#344's head 8387393, the head that merged, passed every test file on Node 24.21.0 and Node 20.20.2 (J10968). The only failures were 69 in a stale dist/test/mcp-modern-http-2025.test.js that the VPS copy kept from anima-research#337's runs. The full check of this commit follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ada-1017 asked for main to come in now rather than after anima-research#282, which lands behind the 0.22.2 release hold, so the PR's next external review reads a current base (room-203 #112135). When anima-research#282 lands, anima-research#360 takes main again. 87 commits since 92125b0, among them anima-research#338, anima-research#274 (TypeScript 7), anima-research#347, anima-research#352 and anima-research#344. The merge is clean: git merge-tree reports no conflict. The merged tree has no reader of ProseStreamRouter, roundDestination or TurnRouting.destination, and main adds no prose delivery site: framework.ts calls routeSpeech eight times on both sides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, as one record per held turn From Anarchid's review of bc3bb8e on anima-research#361 (R1-1 to R1-10). R1-1: the release check took a dry compile that resolved as a fit. context-manager documents that a dry compile doesn't throw an over-budget plan at its plan check (SelectOptions.dryRun), and that previewContext reports feasibility. Just over the budget the dry compile resolved while the turn's committing compile refused, so the hold released into the refusal and held again at compile speed. With the real autobiographical strategy at such a budget, the old hold was held 66 times in a second. - The check now takes the preview's verdict on the agent's next compile, at the budget that compile plans at (Agent.nextCompileBudget, which keeps the tool-result guard's reservation). The preview is synchronous. - A strategy with no preview, or one whose preview needs adaptive resolution, gets the dry compile, whose plan check such a strategy doesn't skip. Only that path awaits, so only it keeps an in-flight field (BudgetHold.checking). The hold is one record per agent with a state (BudgetHold.state: holding, released, spent), in place of the hold map plus the carry, spent, note and gate-closed maps. - released replaces the carry: a refusal of the released turn re-enters the same record, with its first start and deadline. - spent replaces the spent mark. - The note and the continuation flag are fields on it. - The record goes when the turn's stream exists, when the turn fails (inner and outer catch), at a policy veto, and at stop and disposal (settleBudgetHold, dropBudgetHold). - The check's again/rerun and pendingAtStart machinery is gone. pending is read before the verdict, which matters only on the awaiting fallback. A signature (budgetHoldSignature: the strategy's progress snapshot, the agent's runtime settings, and a count of surgeries, undos and redos) decides when the hold looks again. - R1-10: a check is skipped while the signature is unchanged since the last refusal. - A released turn refused again isn't released again until the signature changes. That covers any check/compile mismatch: content that reaches the compile after the check, or a plan the preview can't see (a kv-unified prefix hash). - Undefined, so every check runs, for a strategy that reports no progress. The smaller findings: - R1-6: a held refusal drops the checkpoint its start recorded (dropTurnCheckpoint), so the released start's is the turn's one undo step. It is recorded after the inputs that joined the hold, so undo keeps them. - R1-5: a released or spent record whose turn the inference policy declines ends there. Its logical turn is settled and its record goes, so no note or deadline reaches a later turn. - R1-7: the refused start's own request loses its coalescing subject as well as its batch markers. Its content was stored before its compile, so no cancellation withdraws the turn, and a hold always keeps a cause. A start with no request gets a budget-hold-release request when it is held, not at release. - With the maintenance timer off, a held agent gets one interval that kicks the drain (or with the provider gate closed, only looks) every budgetHoldRecheckMs. - R1-3: BUDGET_HOLD_MS's comment and the changelog now say the bound also ends a compression that is pending but can't move, and that the operator hears of it from the context-refusal alert at each hold's start, at most once per 15 minutes. - R1-2: the changelog says a restart drops the hold, as it drops a turn in flight. Left as they are, with Ada-1017's reasons: - R1-2: in-flight work survives no restart anywhere in the framework. - R1-3: no stall rule: the alert already says it, and the bound ends it. - R1-9: one cause per turn, not a lost or misrouted one. R1-4 and R1-8 come with the merge order: anima-research#352 first, and main's context-manager floor. Tests, 34 in budget-refusal-hold: - The harness's verdict is now the preview, whose contract is to say whether the context fits. The dry compile refuses as the committing one would only on the fallback path, as a hierarchical strategy's does. - A new describe runs the check against the real autobiographical strategy, unpatched but for a stubbed tick: the band where the dry compile resolves and the plan doesn't fit, and the fallback where the preview needs adaptive resolution. - New tests cover: the skip; a re-refused turn held until new evidence; the surgery count across a straddling dry compile; one undo step; the policy veto; a cancellation that leaves the turn its cause; the first kick; and a released turn not shown as waiting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nima-research#352 has landed main took anima-research#343, anima-research#340, anima-research#365 (context-manager ^0.14.1), anima-research#341 and anima-research#352 since 92125b0. The one conflict is anima-research#352's drain kick at the exhaustion site, which this branch moved into kickOverBudgetDrain. The site keeps this branch's call. kickOverBudgetDrain takes anima-research#352's kick whole (no tick started while a surgery holds the store, ticking stops when one takes it, MaintenanceWithdrawnError counted as a stop, the kick registered in overBudgetDrains so a surgery waits for it), and adds only the hold's callbacks. A kick declined under a surgery registers none, and the surgery's release asks for the check. This is the resolution checked in trial merges with anima-research#352's earlier heads, and main's kick block is the same as anima-research#352's head ca2f806. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… store mid-tick, and the surgery waits for it From R1-4 of Anarchid's review of bc3bb8e on anima-research#361: the existing surgery test's ticks resolve synchronously, so nothing showed what anima-research#352's kick does when a surgery arrives while a tick is in flight. The test lets the start pass finish, then makes each tick wait on a gate. A refusal holds the turn, and the hold's kick enters its first tick, with the hold's check waiting on it. A branch switch arrives. After 200 ms it hasn't switched, since it waits for the tick in flight. A kick asked for while the surgery holds the store starts nothing and leaves no callback behind. Once the tick is let go, the kick stops there with no second tick, the surgery completes, the kick's callback has run, and the turn is still held. The hold file has 36 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Anarchid's review of #352 approved it with six minor findings. This answers each of them. R1-1. The host hide command's range form resolved both ends to the first stored copy of their message, so a range ending at a message stored twice (a write cut short, then retried) left the retry in context. The earlier end now resolves to its first copy and the later end to its last, as offline recovery's range does, before the range widens to whole bodies. R1-2. A conversation agent's maintenance calls go through conversationAuxiliaryMembraneFor, which refused a held model but didn't withdraw a call made while a surgery holds the store, as a resident's withAuxiliaryAdmission does. It now withdraws it, so "Maintenance makes no provider call meanwhile" holds for conversation agents too. R1-3. previewAgentRuntimeSettings threw when the agent's context manager refused its queries, as it does while its strategy's initialization is pending or after it failed, though its contract is that preview-layer reasons come back as {available: false, reason}. It now answers that, with the agent's live budget, the patch's path and the transition, all from the agent's own state. A patch the agent refuses still throws, whatever the manager's state, and any other error still throws. resume()'s catch for that case is gone: an unavailable preview already gives no verdict there. R1-4. The 0.21.1 operator note's multiplier reset now says which stores it is for: one that already opened on context-manager 0.14.0 and ran a committing compile. ^0.14.0 now installs 0.14.1, which keeps a still-unstamped multiplier provisional and replaces or drops it on its own. R1-5. A maintenance failure that lasts is met by every pass, and each pass went into the 50-entry maintenance history, so within minutes it pushed out the runs that did work. A pass whose every record repeats a failure already reported, with its report suppressed, now stays out of the history. R1-6. stop() awaited only the maintenance pass, so an over-budget drain kick's compression could still be running when the store closed, and the kick went on ticking after stop() began. stop() now waits for the pass and its drain kicks, and a kick starts no new tick once the framework is stopping. Tests: the range case in operator-surgery's body test; a conversation agent's tick withdrawn under a hold, the history test and a stop() test in maintenance-timer; the preview's answer under a refusing manager in host-quiesce. Two maintenance-timer tests that waited for the history to grow with each repeated failure now count passes, and harnesses that build the framework without start() set running.
Our internal review of the follow-up found these, each fixed here. A hide range missed a copy of its earlier message stored after its later one, and it took what was stored between the later message's copies, which came after that message began. A range now takes what was stored after its earlier message began and before its later one began, from the earlier one's first copy through the later one's first, and every copy of both messages, each whole, as the single form already takes all of a message's copies. Offline recovery's suppressed ranges take the same; before, they ran through the later message's last copy. A pass whose records all met a failure already reported was kept out of the history even when a record had taken a tick first. A pass now stays out only if every record repeats a reported failure and took no tick. No over-budget drain kick starts once stop() has begun, and a conversation agent's compression call made then is refused, as a resident's is. The budget preflight no longer logs that it is applying an update that a refusing manager then refuses. Text: the preview's contract says a patch whose values the agent refuses throws whatever the manager's state, while a patch that sets tail or pace gets no verdict from a refusing manager, since whether the strategy takes them is the manager's to say. reportMaintenanceFailure's comment, the conversation membrane's, contextManagerAnswers' and stop()'s teardown budget now say what the code does. The changelog says when a lasting failure is recorded, and gives the provider gate clause to a resident's call only. Tests: hide's range in both shapes in operator-surgery, and offline recovery's in offline-recovery-branch; in maintenance-timer, a pass that took a tick before the repeated failure stays in the history, a conversation agent's call once stop() has begun is refused, and no drain kick starts then; the stop() test checks that the tick in flight ended before the store closed; in host-quiesce, a budget update under a refusing manager throws and changes nothing.
fix: the findings of #352's review
…ma-research#352's body-aware removals and surgery maintenance waits carried into this branch's marks Main merged 21 pull requests since cd036e0, 0.22.2's release (anima-research#441) among them. The merge conflicted in eight files, each resolved from both sides' intent: - changelog.d/stop-waits-for-operator-operations.fixed.md and switch-branch.added.md: 0.22.2 folded them into CHANGELOG.md, so their deletion stands (Ada-1017, room-203 #127725). What this branch changed in them is described against 0.22.2's published entries. A switch's marks, and the delivery a branch switch or an undo started, are in surgery-marks-one-shot.changed.md and surgery-marks-opt-in.breaking.md already, and the awareness barrier's stop-time behaviour went with the barrier (one-shot: delivery no longer holds MCPL traffic). The one change no fragment carried, a delivery pass that claims nothing more once stop() has begun, is the new surgery-marks-delivery-at-stop.changed.md. - src/api/server.ts: main (anima-research#352) reads the store's branch whenever it reports one, through a getter, and its branch.switch announces its own move, since switchBranch can now move the store after the call returns. This branch removed startDiscordAwarenessDelivery() and said a switch adds or removes no marks. The switch's comment says both, and undo and redo neither set the branch by hand nor start a delivery. - src/framework.ts: - imports: McplRequestError beside main's stderrTailOf, withStderr and McplConnectionCloseInfo. - main moved the surgery read helpers (SurgeryReadableCm, locateMessageIndex, locateMessageIndices, bodyGroupRun) to stored-removal.ts. This branch's copies were the base's unchanged, so they go, and its marks helpers (marksScope, checkedMarks, describeMarksChoice) stay. - host/command hide: main's refusal while a surgery holds the store, and its body-aware runs (every copy of the message, each body whole, removeStoredMessages per run, the hidden count and capped ids), with this branch's marks choice, now scheduled for every message the runs remove, and its operator-log fields (marks, markers). - rollbackToMessage and suppressMessages: main's wait for running maintenance before the switch or fork, then this branch's prepareMarkerBatch. - switchBranch's doc: with main's maintenance wait, the store can move after the call returns, and the doc (main's too) still said it had moved by then. It now says when it moves. Auto-merged, and read: resumePreparedDiscordSuppressions removes each interval through removeStoredMessages, as main's does, inside this branch's completeSuppression path; switchBranch has main's maintenance wait and this branch's no-marks rule. - src/mcpl/server-connection.ts: the event doc has main's (anima-research#399) stderrTail payloads and this branch's reconnect sentence. - src/recovery/offline-branch.ts: both sides' imports. Auto-merged, and read: main's removeStoredInterval loop runs inside this branch's try, after branchAt; main's continuesBody boundary checks and range ends sit beside this branch's carriers. - test/discord-awareness-outbox.test.ts: main's two tests of a resumed suppression (a one-shard body removed as a range; an interval that committed before the crash skipped, and one only partly present refused) are carried over to this branch's journal: storeHarness, a scope, and a start that retires a surgery's batch where main's activated it. Main's stub fields for a surgery's maintenance wait and provider-gate wake (maintenancePass, overBudgetDrains, providerGates) go into hostCommandFramework, which replaced the stub main added them to, and the wait's two fields into this branch's stub for a switch onto a pending suppression's target, since switchBranch now waits too. - test/offline-recovery-branch.test.ts: both sides' new tests. Main's four that read result.refs to name what a recovery removed, the three in the conflict and "a suppressed range takes every copy of both its messages" beside it, choose marks: { scope: 'all' }, as this branch did for the base's own tests, since refs now lists only the refs a recovery marks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main comes in now rather than after anima-research#282, which lands behind the 0.22.2 release hold, so the PR's next external review reads a current base. When anima-research#282 lands, anima-research#360 takes main again. 87 commits since 92125b0, among them anima-research#338, anima-research#274 (TypeScript 7), anima-research#347, anima-research#352 and anima-research#344. The merge is clean: git merge-tree reports no conflict. The merged tree has no reader of ProseStreamRouter, roundDestination or TurnRouting.destination, and main adds no prose delivery site: framework.ts calls routeSpeech eight times on both sides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This PR fixes the places that still assume context-manager 0.13's contract, though agent-framework has required
^0.14.0since 0.21.1, and the places that remove a single message from a body, which context-manager refuses:resume()no longer throws when one agent's runtime settings can't be read. It warns, naming the agent, and proceeds without a feasibility verdict for that agent. While a surgery holds the store, it refuses as busy, including when the surgery took the store whileresume()waited.hidecommand, a module's removal (a message deleted on Discord) and the coalescer's. A rewind takes the whole exchange, bodies included, in one removal.It also adds notes for operators and hosts to CHANGELOG's 0.21.1 section, about what context-manager 0.14.0 changed for them, including stores whose estimator calibration 0.14.0 can reject. No dependency changes.
Merge order: after #286, and before the fix for #326 (below).
Why these places
context-manager 0.14.0 refuses most of its methods while its strategy's initialization is pending or has failed. Initialization is pending only inside
ContextManager.open,switchBranch,forkandsetStrategy, and a failed one leaves the manager refusing until a successfulswitchBranchorsetStrategy. The refused methods includeisReady,getPendingWorkandgetHotContextSettings, which 0.13 always answered. A strategy can also refuse on its own: the autobiographical strategy refuses its queries for a branch it hasn't reloaded.0.21.1 raised the floor to
^0.14.0and changed nothing else, so:isReadywhile building its queue, outside any guard. One agent whose manager was mid-switch or had failed to initialize ended the whole pass. No agent's maintenance ran, and the pass raised one alert for the whole pass (scope: 'pass'), on every pass, every few seconds by default, for as long as the manager stayed refused.resume()reads each agent's runtime-settings preview for its feasibility check. One agent's refusal threw out ofresume()before any verdict, andforcecouldn't get past it.continuesBody). A document posted twice is stored as two copies under one group id, and 0.14.0 reads the copies as two bodies. Offline recovery compared group ids alone, which 0.14.0's changelog asks hosts to stop doing.Separately, context-manager's
removeMessagerefuses any message that carries a group id, a one-shard body's included, and has since before 0.11. Four places in agent-framework removed a single message with it, and each failed on a shard.What changes
Context maintenance (
src/framework.ts)error) and its owncontext-maintenance-failedalert withscope: 'agent', and sits the pass out. The other agents run. Anything else that throws while one agent's maintenance is prepared, such as its tool refresh, is that agent's failure in the same way.errorand alert, as any tick failure is.rollbackToMessageorsuppressMessages) orswitchBranchholds the store, no pass starts and no over-budget drain kick starts, and a running pass or kick starts no new tick. The operator call waits for them to finish before it switches or forks the branch, so no maintenance work overlaps the move. Maintenance makes no provider call meanwhile. One made then, or one waiting at the agent's provider gate when the hold is taken, is withdrawn without being sent. Such a call waits behind a cooldown, or behind the retry turn a cooldown queues, which the hold keeps from starting, so waiting for it could wait for good. A withdrawal is no maintenance failure, and context-manager counts no attempt for it. A merge keeps its queue entry, so the next tick after the hold does it. A chunk's compression has already left its queue: context-manager logs "Failed to compress chunk (hierarchical)" with the withdrawal and records the call. The chunk, still uncompressed, is queued again at that manager's next rebuild: the switched manager's own initialization, or another agent's next message or sent compile. So the operator call's wait is at most the provider calls already in flight. A pass waits for every agent's work to settle before it ends. The store moves that take no reservation, the API'sbranch.switch,undoLastTurnandredo, are After a surgery or branch switch, the other agents on the store can't compile until the host restarts #326's. A failure the pass meets meanwhile is reported as any other.maintenanceTick()answersran: falsewith a warning that names the surgery.getContextMaintenanceSnapshot()reports an agent whose readiness can't be read asready: falsewith anerror. A strategy whosegetProgressSnapshotthrows reads as having no progress, in the snapshot and in the pass's records.The API server (
src/api/server.ts)switchBranchnow moves the store once maintenance already running has finished, so a switch's move can come after the call returns. The API server reads the store's branch whenever it reports one:branch.current, the branch list,/health,/statusand a client's welcome. It keeps the last name it read only for once the store has closed.branch.switchannounces its own move, from the branch it began on to the one it was asked for, once the move is made. One that fails after moving the store still announces it, and a refusal announces nothing.resume()resume()warns, naming the agent and the error, and gives that agent no verdict, as it already does for a preview that isn't available. The verdict guards against returning an agent that would run over budget, and an agent whose manager refuses its compiles can't run. That manager refuses the agent's turns and writes until aswitchBranchorsetStrategysucceeds, and the maintenance pass reports it.resume()refuses withOperatorActionError(agent-busy), as the other operator calls do, since the manager the surgery is switching refuses its settings read until the switch is done. It checks on entry and again after its waits for a pass or a preview, before it reopens anything, so a surgery that took the store meanwhile is refused too.Offline recovery (
src/recovery/offline-branch.ts)continuesBody(anchor, next). An anchor at the end of one copy of a reposted document is accepted, and an anchor inside a copy is still refused.Removing one message of a body (
src/stored-removal.ts)removeStoredMessagesremoves a single message outside any body withremoveMessage, and a shard, or any longer interval, withremoveMessages, which refuses an interval that splits a body.removeStoredMessageremoves a message by its id, with the rest of its body when it's in one. It finds the body in blob-free windows, as the live surgeries do, rather than reading the whole branch with every attachment on it, once for each message a sweep removes.removeMessagefor a single message, so suppressing a one-shard copy failed at removal.removeMessage, which the Discord module calls for a message deleted on Discord (live, in its sweep, and at reconcile), and the coalescer's removal of an unread message it replaces go throughremoveStoredMessage. A long message, stored in shards, used to stay in context when it was deleted.hidecommand hides a message stored as a body whole, and every stored copy of it: a write cut short and retried leaves a partial body before the whole one, both carrying the Discord id. Only a message that carries a Discord id matches. Its reply counts a body once and lists each Discord message once. Its range form widens both ends to whole bodies. Its operator-log entry lists at most 50 of the ids it removed, withremovedIdsTruncatedwhen there were more, as the range form's does. While a surgery holds the store, it refuses, as the other operator calls do.bodyBoundsIn) and describes it whole. A tool result's paired tool_use turn is taken whole too, since a turn stored as a body carries its tool_use on its first shard, and the exchange goes in one removal, so a refusal can't leave it half applied. It used to fail at removal.Changelog
changelog.d/cm-0.14.changed.md,changelog.d/cm-0.14.fixed.mdandchangelog.d/body-removals.fixed.mddescribe the changes above for the next release.OverBudgetError. The note says how to tell the two apart and points at the Operators entry of context-manager 0.14.0's changelog for the repair.Where else agent-framework meets the refusals
Each of these was read at this PR's base and left as it is:
getRuntimeSettings, the runtime-settings preview,updateRuntimeSettings,resetRuntimeSettingsandcancelRuntimeSettingsTransition). During a switch, or after a failed initialization, the operator gets context-manager's refusal, which names the branch and how to recover.editMessagereaches the primary agent's manager, and a refusal comes back to the module.message.listand agent-context commands compile through the context manager, and a refusal comes back as the command's error.main, the resume flush logs it and drops it from the durable queue, and the withdrawn-turn flush at a turn's start throws at the first refused write, losing the rest of its drained batch. Since this PR letsresume()go on past an agent whose manager refuses, it merges after fix(deferral): hold window-only messages to the turn's end; injected messages are read #286, which keeps a write that can't be stored in its agent's queue on every flush path, holding what's queued behind it.branch.switch,undoLastTurnandredomove the store directly. Each manager that didn't make the move keeps its strategy loaded for the old branch, and the autobiographical strategy then refuses until it's re-initialized, on context-manager 0.13 as on 0.14. That's After a surgery or branch switch, the other agents on the store can't compile until the host restarts #326, whose fix re-initializes those managers and merges after this PR. Until then, this PR keeps maintenance running for the agents that can. The direct moves don't hold the store, so they don't wait for a maintenance tick either; After a surgery or branch switch, the other agents on the store can't compile until the host restarts #326's fix gives them the reservation.switchBranch, which re-initializes it. If the restore fails too, the operator's error says so, as before.main. It lands on the branch the surgery is leaving, or is refused mid-switch, and the Discord module's sweep tries again later.hidetakes both. Taking both on a module's removal would need a removal by Discord id.Tests
test/maintenance-timer.test.ts:resume()refuses as busy meanwhile; a rollback that takes the store whileresume()waits for a tick, refused byresume()after the wait; a rollback that waits for an over-budget drain kick's tick, after which the kick ticks no more;test/stop-with-host-operations.test.ts: an API switch made while a pass runs leaves the store and the current branch onmainuntil the pass ends, then announcesmaintoside, with the current branchside.test/host-quiesce.test.ts:resume()with an agent whose manager failed to initialize, and the warning that names it;maintenanceTick()while a surgery holds the store.test/offline-recovery-branch.test.ts: two copies of one document, for an anchor at the end of a copy, an anchor inside a copy, a suppression of one copy (reopening the recovered store to check which copy remains), a suppression that splits a copy, a split across a scan window's edge, a one-shard copy, a plain message removed alone beside a one-shard copy removed as a range, and an anchor at the end of a write cut short before its retry.test/discord-awareness-outbox.test.ts: a resumed suppression removes a one-shard body as a range and a plain message alone; it skips an interval that committed before the crash and refuses one only partly present.test/operator-surgery.test.ts:hideremoves a one-shard body, a three-shard body counted once, and a cut-short copy with its retry; its range form widens both ends; it refuses under a reservation,"undefined"matches none of two id-less messages, and the held message hides once the store is let go; a module's removal of a body's shard takes the body, with no full read of the branch; a singlehideof a 60-shard body logs the first 50 ids andremovedIdsTruncated; a rewind takes the whole body of its newest shard, and a sharded tool exchange in one range.Evidence
At
fb8a6c4f, which containsmainatcd036e0d, each test file in its ownnode --testprocess on a Linux VPS, after a clean typecheck and build, with context-manager 0.14.0 and membrane 0.5.87:--test-force-exit. On Node 24 with--test-force-exit, as CI runsnpm test, 2166 ran and passed: it cutagent-runtime-settingsshort (40 of 55), which passes whole unforced (npm test's --test-force-exit can end a test file before its tests report, so a short run still passes #294).framework.ts,api/server.tsandstored-removal.tsput back tod71370d5, all nine tests the round adds or extends fail: six inmaintenance-timer, two inoperator-surgeryand one instop-with-host-operations. Nothing else in those files fails.194b17a6, and the withdrawal's pieces again atfb8a6c4f, with the loop's own. 42 fail a test. Two don't:hide's widening of each matched copy to its body. context-manager's chunker gives every shard of a body the message's metadata, so matching every stored message by its Discord id already takes each whole body, and the widening only guards a body whose shards don't all carry the id.ca2f8068, afterfb8a6c4f, changes onlytest/maintenance-timer.test.ts. Six tests now read the first pass the timer recorded rather than the newest, since on a busy machine a second pass can end before the read. One of them failed that way in a cross-package run. The file passes 27/27 against context-manager 0.14.0 and against itsmainat58a3f9e.Greptile's reviews of this pull request and of #353, which is built on it, found the five problems that
3abf2bd1to609b247dfix: a surgery's wait for maintenance parked at the provider gate, the API server's branch read before the move, the reminder's count, the full read inremoveStoredMessage, and the singlehide's log.Read by Gladys-2413 (Claude Opus 5.5), who read every round and ran the changed files on both Nodes. Her probes found the two
hidegaps this pull request closes:hideunder a surgery's hold, and a missing Discord id read as "undefined". Her read of the answer to Greptile found that its text said context-manager keeps a withdrawn chunk's work queued, which holds for merges alone, and suggested the loop the gate's wait now takes (fb8a6c4f).@antra-tess, @Anarchid: I'll merge it once its checks pass on its merge with the then-current
main, after #286, and not before 2026-10-15 16:00 UTC, unless you'd like changes first.— Ada-1017 (Claude Opus 5.5)