Repository navigation
fix(boot): restored state that can't be read or taken fails create(), naming it, not an empty state - #454
Conversation
…g the state, not an empty state At boot, these reads took a failed read of the framework's own state, or a stored value in a form the code doesn't read, as "nothing stored", and the framework served without it: - Host mode (readHostMode): a quiesced host booted serving, and the writes it had deferred during the maintenance landed at that boot. A host-mode document that isn't a record, or whose quiesced isn't a boolean, read as not quiesced. - Deferred writes (restorePersistedDeferredWrites): the queue restored nothing, and the next deferral replaced it. A context that couldn't be scanned for writes that landed before their ack was counted as holding none, so such a write landed a second time at resume. - Usage totals (restoreUsageState): they started at zero, and the next inference's persistUsageState stored zeros over them. - An agent's persisted runtime settings (createAgent): the agent booted at its recipe's settings, and its next settings change stored over the record. - MCPL coalescing (initializePushCoalescer): an acknowledged deferred acceptance or a pending batch was never delivered, and the next receipt or flush replaced the stored state. A snapshot of a version this code doesn't read was ignored and replaced. Each now throws an error that names the state (or its recovery file), says why the framework doesn't start without it, says it's unchanged, and says what to do. In create(), that fails create(). The coalescing state is read before the coalescer is built; on a framework started with no MCPL servers that read runs at the first connectMcplServer, which fails instead and leaves the subsystem uninitialized, so the next connect reads again. Nothing stored still starts empty, and entries inside a value these reads take (one queued write, one agent's usage) are checked as before. The persisted tool_result_inline_max_chars caps lose nothing when their read fails, since persistToolResultInlineCap reads them again before it writes one, so that read is logged and boot goes on, as before. usage-tracker.ts exports isPersistedUsageState, the check its restore applied, so the boot and the tracker agree on what stored totals are. jsonKind names what a form check found without its content. The writers of these states, from the release each first appears in through main, write only forms this code reads, so none of the form checks fires on a store a release left. Not taken: reading these early and closing an owned store (a failed create() that releases what it acquired covers every such failure); booting quiesced on an unreadable host-mode record (it would hold every data plane on a host that may never have been quiesced); an error class (nothing branches on one). Tests: test/boot-state-read-errors.test.ts, one or more per site: a boot with that read failing, or its state in a form this code doesn't read, fails naming it and leaves the state as it was, and the next boot that can read it restores it; the caps' log line; the runtime connect; and a control that nothing stored starts empty. agent-runtime-settings' "a record the boot can't read is logged, and the agent boots at the recipe's" turns round, and a record that isn't an object is added. test/helpers/ failed-create.ts stops what a failed create() built, so a test can boot again on its store while a failed create() still keeps it open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…managed MCPL state after a restart
The first commit's boot reads still took some states as empty, or
refused in ways their text didn't describe, and three more reads of
restored state did the same. This round:
Recovery files. readRecoveryFile asked existsSync first, which answers
false for any error (EACCES, ENOTDIR, EIO) as for a missing file, so an
unreachable host-mode or deferred-writes file read as absent and the
store's slot was read in its place. It now reads the file and takes only
ENOENT as no file; any other error throws. Text that isn't JSON throws a
StoredFormError, so the refusal says the file starts the framework once
it holds a record, since reading it again won't change it. Both readers
call it without an existsSync gate, and fall back to the slot only when
the file is missing, which also covers a file removed between a check
and the read.
Stored nulls. Nothing stored is no file, no key in framework/state, or a
state the store holds nothing for (getStateJson's null). A null someone
stored in a file or under a key is a present value no release writes:
the deferred-writes file holding null, usage: null, and an agent's
settings record of null now refuse, as the host-mode read already did.
framework/state itself. A framework/state that isn't an object read as
holding nothing at every reader, and the first write replaced it.
restoreUsageState is create()'s first read of it, so it refuses there,
naming framework/state.
Usage totals. persistedUsageProblem (replacing isPersistedUsageState)
says what keeps a stored record from restoring: not a record, totals
that aren't token counts, or a byAgent that isn't a list (which was
skipped, and the next persist stored an empty list over it). A byAgent
entry that isn't an object is skipped, as invalid entries already were,
where a null one threw an unnamed TypeError. A provider's usage can lack
a count (membrane 0.5.87's anthropic and bedrock adapters pass a whole
response's input_tokens through as reported), which made the totals NaN,
stored as null; accumulateUsage now adds 0 for a count that isn't a
finite number, and a count stored as null restores as 0, so totals an
earlier release stored that way don't refuse the boot.
Agent settings. A map of agents' settings that isn't an object refuses,
like a record that isn't one (the next settings write spread it and
stored over it). The record is read by own key only, so an agent named
"constructor" has no inherited record. Form problems throw a
StoredFormError, so the refusal can say whether starting again helps.
Tool-result guards. adoptToolResultGuard read toolResultGuards through
optional chaining, so a container that isn't an object read as no
setting, and the next guard change spread it and stored over it; and
once any guard was stored, an agent named "constructor" read
Object.prototype.constructor and failed create() as an invalid guard. A
container that can't be read or isn't an object now refuses, naming it,
when an agent is created (at boot or later, as for a fork), and the
setting is read by the agent's own name.
MCPL coalescing. restorePushCoalescer reads, checks, builds and restores
the coalescer, and initializeMcpl calls it before it assigns anything
else; installPushCoalescer installs it where the old initializer ran.
Before, a refusal left the registry, feature-set manager, hook
orchestrator and emitter assigned, and guards that test only the
registry then took MCPL as up after a refused runtime connect. The
coalescer's wasPublished caught a context it couldn't read and answered
"not delivered", so a published batch or a receipt's occurrence was
delivered again; restore is its only caller, so it now throws, and the
restore refuses naming the agent. A snapshot version that isn't a number
reads as "a snapshot whose version is <kind>, not 1".
MCPL checkpoint trees. CheckpointManager.loadFromStore returned at
!data?.trees, so a stored mcpl/checkpoints without trees read as no
trees, and the next feature set registered stored over it; a failed
read threw from the middle of initializeMcpl. Trees that can't be read,
or a stored value that isn't an object whose trees is an object, now
refuse, naming the state, and initializeMcpl builds the checkpoint
manager right after the coalescer, before anything else, so either
refusal leaves nothing of MCPL built. Every release writes { trees }.
Host-managed MCPL state. loadFromStore restores each tree with
currentState undefined, to be read "during registerFeatureSet", but
registerFeatureSet returned early for a tree it already held, before
its state read. So after a restart a host-managed feature set's state
read as empty, and its next delta checkpoint applied to nothing and
stored over it (shown on main with a real store opened again). A stored
tree's state is now read when its feature set registers, through
loadHostState, which a new tree uses too; a read that fails throws,
naming the state, so its server's connect fails. A first-time tree is
held only once its state is read, so a registration that fails is made
whole, stored tree and trace included, by the next one.
checkpoint-manager.ts gets its own thrownText and storedKind, as the
repository's modules keep such helpers.
Texts. A failed read keeps "starts once it can be read". A form problem
says the framework starts once the file holds a record, or, for a store
state, "starting again won't help until it's repaired". A recovery
file's text now says that moved aside, the framework reads the store's
slot instead, since that's what it does. The coalescing and checkpoint
texts say that create() with MCPL servers configured, or a first
connectMcplServer(), fails with MCPL. The deferred-writes check's text
names the slot when there's no file.
Tests: boot-state-read-errors grows to 26 tests: an unreachable
host-mode file (ENOTDIR) fails create() and the next reachable boot is
quiesced; both recovery files holding text that isn't JSON; the
deferred-writes slot's failed read and form without a recovery file;
usage forms (null, byAgent) and a null count and a null byAgent entry
restoring; a framework/state that isn't an object; the tool-result
guards' failed read and forms; a context that can't confirm a receipt's
delivery; checkpoint trees that can't be read or aren't trees; a
host-managed feature set's state read after a restart, and its failed
read for a stored tree and a first-time one; the coalescing states and
the checkpoint trees at a runtime connect, with nothing of MCPL built
after the refusal; the forms releases wrote and a store from before the
recovery files booting as before; and an agent named "constructor" with
settings and guards stored for another. agent-runtime-settings adds a
null record and a map that isn't an object. usage-tracker adds a usage
without a count. failed-create.ts's comment names the pull request that
fixes the issue it cites.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nnects, and keep NaN out of the usage cost The fix round's review found that a restored host-managed tree's state was still read only when its feature set registered at a connect, after the server was added. A connect that returned before registering, a feature set now declared with neither flag, or a read that failed with the connection left up all left a tree unread while checkpoints could still be recorded over its state; and at boot the read's failure was logged by initializeMcpl's fail-open per-server loop, so create() went on without the server. CheckpointManager.loadHostStates(serverId) reads the stored state of every host-managed tree held for a server, whatever its feature sets declare now, finding the server's trees by their key prefix as removeServer does. initializeMcpl calls it for each configured server right after the checkpoint manager loads the trees, before anything else is built, so a failure fails create() before any server connects; a runtime connectMcplServer() calls it before the server is added, so a failure leaves nothing connected. registerFeatureSet returns for a held tree, as on main, since loadHostStates has read it before any connect registers it; the round's read there was a second site that no path needed, and missed a tree registered by no declaration. It still reads a first-time tree's state. The host-state refusal now says the server doesn't connect, and neither does create() with it configured, until the state can be read; loadFromStore's comment says where a restored tree's state is read. Usage. accumulateUsage adds 0 for a cost total that isn't a finite number, as it does for a count: membrane computes the cost from the counts, so a missing count made the cost NaN. parseUsage reads a cost total stored as null as 0, as it does a count. Texts. The recovery files' refusals no longer say that moved aside the framework reads the store's slot: with a recovery path the slot doesn't hold the file's record, so following that would boot a quiesced host serving or drop the queue. The tool-result guards' refusal says the agent isn't created, since without them it would take its recipe's guard. The usage tracker's comment names the reachable missing count, Bedrock's streaming message_delta in membrane 0.5.87; the framework streams every inference, so a whole response's usage never reaches it. The changelog fragment says when host-managed state is read, and covers the cost. Tests: at boot, a configured server's restored host-managed state that can't be read fails create(), and the next boot reads it, with the fixture's feature set declared with neither flag; at a runtime connect, it fails the connect before the server is added, and the next connect reads it. The CheckpointManager tests read a restored tree's state through loadHostStates, and its failed read fails loadHostStates. A deferred-writes file behind ENOTDIR fails create(). A null hostMode in framework/state refuses. A stored null cost total restores as 0, and a NaN cost or NaN cache counts add nothing. The runtime connect test is renamed for what it covers. Texts in the existing tests follow the refusals. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…own cost unknown Round 2's review found the text claimed more than the code does for a host-managed feature set with no stored tree. loadHostStates reads the trees restored from mcpl/checkpoints before a server connects, but a feature set with no tree, registering for the first time or after a disconnect's removeServer dropped its tree, is read when it registers, once its server has connected, as on main. A failed read there fails that server's connect; at boot initializeMcpl logs the connect's failure and goes on, as for any server's, and the connection stays in the registry, which is a registration failure's own handling, outside this change. Its state isn't taken as empty: registerFeatureSet holds the tree only once the read succeeds, so the next connect reads again. So loadHostState now takes where it's read from. A restored tree's refusal says neither create() with the server configured nor a connectMcplServer() of it goes on without the state, and that they go on once it can be read; a registration's says the feature set isn't registered, so the server's connect fails, and the next connect reads it again. The comments at initializeMcpl and runConnectMcplServer say the same, including that a runtime connect reads its server's trees once MCPL is up, so a refused one leaves MCPL up, as a first connect brought it up. loadFromStore's comment and the changelog follow. loadHostStates leaves a tree whose state is in memory alone: connecting a server again read its trees again, and could replace a connected server's live state with the stored one, or refuse with a read error where the duplicate connect's own refusal belonged. Usage cost. accumulateUsage added 0 for a NaN cost, so an inference whose cost membrane couldn't compute made the session's cost a known zero when it came first. A cost that isn't a finite number is now an unknown cost, left out as an absent cost already is. parseUsage keeps a stored cost only as a finite total with a string currency; anything else, such as the null a NaN total was stored as, or a string, which the next add concatenated, restores as no cost. The count comment no longer calls a null count's 0 the count that survived: the sum before it was lost. The tool-result guards' form refusal says the host's operator repairs them, since it also reaches agents created later, by callers that can't. Tests: the boot refusal for a restored tree asserts the fixture's server saw no initialize. The CheckpointManager tests take each read's own text. A new test pins which trees loadHostStates reads: only its own server's host-managed ones, not another server's or a rollback-only tree, and none whose state is in memory. The usage test drops the output count, as Bedrock's message_delta can, and checks a NaN cost leaves the session's cost unknown when it comes first; the restore test puts a null and a string cost in agents' entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ect isn't read, and the cost is left as main has it Round 3's review found: The guards' new remedy text wasn't followed into its test, so boot-state-read-errors failed at round 3's head. The test now expects "won't help until the host's operator repairs them". A registration's refusal said the server's next connect reads the state again. A failed registration leaves the connection in the registry, so a plain connectMcplServer() of it is refused as already registered until it's disconnected; a reconnect or a restart registers the feature set again. The refusal now says the state is read again when the feature set next registers, and the changelog and the description say what re-reads it. The changelog's comparison with main names main's order: it held the tree before the read, so after a failed read the state was taken as empty. A runtime connect of a server already connected still read its trees whose state wasn't in memory, such as one with nothing stored, so a failed read replaced addServer's "already registered" refusal with a read error whose text promised the connect would go on. runConnectMcplServer now reads only for a server the registry doesn't hold. The cost. Rounds 2 and 3 changed how an unknown cost reads: first adding 0 for a NaN cost, then leaving it out and restoring a cost that isn't a finite total with its currency as none. Each answer left a partial sum reading as a known one, since the tracker has no way to say a session's cost is unknown for good, and that's the same question as the cost after a currency mismatch, which the owner placed as its own item. This change doesn't need either: the boot's usage check never looked at the cost. So accumulateUsage and parseUsage handle the cost as main does, and the description's Not covered names the question. Tests: a connect of a connected server whose tree holds nothing is refused as already registered while a read of that tree would fail. The registration text's test follows the new wording. The usage tests no longer set or check a cost; the restore test puts a null count in an agent's entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…not removal
The likeliest way an operator meets these refusals is a recovery file a
power loss left empty or torn, and its refusal said only that the
framework starts once the file holds a record. The obvious repair,
removing the file, is the wrong one: without the file the store's slot
is read, and with a recovery path the file's writes don't keep that
slot. quiesce() writes only the file, so a host quiesced mid-maintenance
would boot serving.
So the host-mode file's form refusal names the record to write,
{"version":1,"quiesced":true} to boot quiesced or
{"version":1,"quiesced":false} to serve, and says removing it doesn't
help and why. The deferred-writes file's names {"version":1,"pending":[]}
for none, and says the same of its slot. (Ezra-2423's read, which Ada-1017
asked be taken.)
Tests: the two form tests expect the new texts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The deferred-writes file's form refusal said removing the file doesn't help. Removing it does let the framework start: it reads the queue from framework/deferred-writes instead, which the file's writes don't keep, so it restores whatever older queue is there, or none. An operator who removed it would see the text's prediction fail, and the text didn't say what the harm is. It now says what removing it does. (Ezra-2423's read of 86b1803.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[High impact] The changes reach framework startup and MCPL recovery across configured hosts. Appears safe to merge, with a non-blocking fix to keep malformed optional caps from stopping boot. Findings
Fix with agent prompt### Issue 1
src/framework.ts:5818-5819
Moving `Number(value)` outside the catch lets a malformed optional cap stop `create()` with an unnamed `TypeError`. For example, `{"toolResultInlineCaps":{"agent":{"toString":null}}}` reaches this loop, but its value cannot become a number. Previously the catch allowed boot to continue.
Check the value's type before converting it, or catch and log conversion failures per entry. Add a test for this stored form.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.SummaryThis PR stops boot from treating unreadable required state as empty.
Validation: inspected the changed paths and confirmed the cap conversion error in an isolated Node check. The full test suite was not run. Recorded acknowledgments from
Reviews (1) · Last reviewed commit: "fix(boot): the queue file's refusal says..." · Reviewed by Greptile |
| for (const [name, value] of Object.entries(data?.toolResultInlineCaps ?? {})) { | ||
| const n = Number(value); |
There was a problem hiding this comment.
Moving Number(value) outside the catch lets a malformed optional cap stop create() with an unnamed TypeError. For example, {"toolResultInlineCaps":{"agent":{"toString":null}}} reaches this loop, but its value cannot become a number. Previously the catch allowed boot to continue.
Check the value's type before converting it, or catch and log conversion failures per entry. Add a test for this stored form.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/framework.ts
Line: 5818-5819
Comment:
**Malformed caps can stop boot**
Moving `Number(value)` outside the catch lets a malformed optional cap stop `create()` with an unnamed `TypeError`. For example, `{"toolResultInlineCaps":{"agent":{"toString":null}}}` reaches this loop, but its value cannot become a number. Previously the catch allowed boot to continue.
Check the value's type before converting it, or catch and log conversion failures per entry. Add a test for this stored form.
---
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.
Addressed in eea1d3f4. Each entry's Number(value) now has its own catch. The read then drops and logs a stored cap it can't convert, such as {"toString":null}, like any other invalid cap, and applies the others.
The new test in test/boot-state-read-errors.test.ts, "a stored inline cap that can't become a number is dropped, loudly, and the others apply", stores {"agent":{"toString":null},"other":5000}. create() succeeds and logs the drop, and other's cap is 5000. With that catch taken out, the test fails with Cannot convert object to primitive value thrown from create() (Linux, Node 24.22.0).
— Imogen-2490
… not thrown from create() From Greptile's review of anima-research#454. Narrowing the caps read's catch to the read itself left each entry's Number() outside it, and Number() throws for an object it can't make a primitive of, such as {"toString":null}. Such an entry then failed create() with an unnamed TypeError, where main's catch around the whole read dropped every cap silently. The conversion now has its own catch, so that entry is invalid like any other: dropped, loudly, while the other caps apply, as the read's rule says. Test: a stored caps record holding {"toString":null} for one agent and 5000 for another boots, logs the drop, and applies the other's cap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI on anima-research#454 failed two tests in stop-with-host-operations at cb2a26c and eea1d3f: "stop() waits for an MCPL connect already running before it closes the connections", and the same for a restart. Since 246073e, runConnectMcplServer asks the registry whether the server is connected already before it reads the server's stored trees. Those tests' fake registry had no getServer, so the connect threw "this.mcplServerRegistry ?.getServer is not a function", and the connect test then timed out waiting for the operation it starts. The fake now answers null, as McplServerRegistry.getServer does for an unknown server. The source is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main gained, among others, anima-research#453 (F134: quiesce() writes its record first), anima-research#426 (a failed create() releases what it acquired) and c9d612e (the deferred queue's recovery drops entries the store can't take, one entry per id, and numbers seqs that aren't valid after the others). One conflict, in restorePersistedDeferredWrites. main changed the per-entry handling inside its catch around the whole restore, which logged any failure and restored nothing; this branch had replaced that catch with refusals for a failed read, a form that isn't a queue, and a failed check for writes that already landed. The resolution keeps this branch's refusals and takes main's per-entry handling whole between the form check and the landed check: wellFormed and its dropped-entry lines, one entry per id, the seq numbering with deferredSeq set from lastSeq, and the rewrite when entries were dropped, repeated or renumbered as well as when some had landed. Entries inside a queue the boot takes keep main's handling, as this change's rule says. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With anima-research#426 in main, a failed create() stops the framework it built and closes a store it opened, so a test can boot again on that store. The helper no longer needs to find the framework through restoreUsageState and stop it: it asserts that the build fails with the expected message, and stops a build that succeeds, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… leave Since main 86a3683 merged in, stop() drains every agent's deferred writes and, on a quiesced host, defers them again, handing them off at the agent's current position. The landed-write test builds "a write that landed before its ack" by adding the copy through a staging framework and stopping it, so staging's stop now moved the receipt's scanFrom past the copy ({"agent":1}), and the next boot's check for landed writes started after it and restored the write again (the test saw one deferred write where it expects none). A crash between a write landing and its ack leaves the receipt its hand-off recorded, from before the copy landed, so the test now writes that receipt back (scanFrom {"agent":0}) after staging stops. The refusal it checks, and the next boot's skip, are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main gained anima-research#457, anima-research#458, anima-research#459, anima-research#454, anima-research#460, anima-research#464, anima-research#412 and anima-research#467 since 86a3683. One block conflicts, in restorePersistedDeferredWrites: anima-research#454 narrowed its try to reading the recovery file, so that a file it can't read or take fails create(), and moved the parsing and numbering after it, while this branch changed that numbering (boot numbers the restored queue 1..N). The resolution takes main's side of the block, then makes this branch's change to main's numbering where it now stands: the queue is ordered by valid seq, then the rest in file order, numbered 1..N after the already-landed entries are left out, and the counter goes on from N; the file is rewritten for an entry without a valid seq, not for the new numbers. main's check for writes that already landed, which now refuses create() when a context can't be read, is kept as main has it, over the ordered entries. Everything else merged without a conflict. The only other change of main's within forty lines of one of this branch's is anima-research#457's to an ephemeral run's idle watchdog, which now rejects a run that stop() has begun to end as stopped; it sits above the run's finally, which this branch changes, and neither depends on the other. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main brings agent-framework anima-research#456 (the workspace file tools' fixes), anima-research#460 (editors' files and nested mounts), anima-research#410, anima-research#455, anima-research#457, anima-research#458, anima-research#459, anima-research#454, anima-research#464, anima-research#412 and anima-research#467. - write, edit and delete: anima-research#456 answers a change stored in the workspace but not on disk with one frame (storedButUnfinished), which leads with "stored, so don't send it again" and gives the push's reason. This branch had reworded those frames to say where force goes. Its frame goes and anima-research#456's stays: every reason a write's push can return names materialize where it offers force, and so do this branch's mode notes. Each success keeps its modeKept note, and commitToolChange keeps anima-research#456's onCommitted and answers { refused, modeKept }. - write: an overwrite keeps the entry's mode inside anima-research#456's guarded commit, as an edit does. - reconcile.ts: an effect that fails untouched names its kind, a chmod step's included, beside anima-research#456's account of what's in the way (inTheWay). - edit: anima-research#456's edit takes a change on disk in first, with a pass over its path. Where disk changed only a file's mode, that pass takes the mode in, as a listing does, and the edit's note read it as disk's copy having changed and the edit applying to that text. A mode taken in alone isn't a change to the text, so the note no longer counts one. The edit then writes at the mode disk has and leaves nothing owed. - anima-research#460: a push skips a path in another mount's directory before it plans, so it plans no chmod there either. - The changelog fragment drops its sentence about the write tools' frame, which anima-research#456's replaces, and no longer names an edit among what says that nothing records which side changed a mode after a refused widening, since an edit's pass takes disk's mode in first. - Tests: the test of a refused narrowing's frame takes anima-research#456's wording, the test of the force sentence goes with that sentence, and two tests pin an edit after a mode disk changed and after a refused widening. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-b to fix-d (R2-1) The conflict, and what it keeps: - src/framework.ts after thrownText: main's StoredFormError and jsonKind (anima-research#454), and not handlerFailureText, AnswerOnce or answerOnce, which this branch replaces with its contained responder. main left those three as they were, so dropping them drops nothing of main's. Every inbound request handler in the merged wiring still goes through onRequest, and main adds or changes no handler registration. This is the resolution the maintainer's second review gave (R2-1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s close of a failed runtime connect, and train fix-c and after Conflicts, and what each keeps: - settlePolicyExchange's tail: this branch's. main's side there is the barrier's release, completion and failure (installMcplDataPlaneGate and the rest), which this branch removes; anima-research#459 changed only a comment in it. - connectMcplServerInternal's tail: this branch's, with no data-plane gate. main's side is the gate's install, its accounting wait and its completion, with anima-research#459's handling of a failed step inside it. - test/mcpl-awareness-barrier.test.ts stays deleted, with the barrier it tested. Carried from main (anima-research#459, R2-1 of the maintainer's review of 3fb0415): - A runtime connect or restart whose policy registration throws closes the new connection for good through the registry (closeFailedRuntimeConnect), then rethrows, so it isn't left connected and half registered, and a later connect of its id isn't refused as already registered. Here that wraps the connect's settlePolicyExchange for a connect that isn't staged, as main's close is. A staged startup connect's failure goes to initializeMcpl's loop, which logs it and goes on with the other servers, on main as here. anima-research#459's other failed step, awareness accounting, has no counterpart here: no connect accounts for awareness. - anima-research#459's tests that don't need the barrier move to mcpl-reconnect-registration: a runtime connect whose policy registration throws closes its connection for good and a later connect of its id reaches the server; one whose close then fails stays registered and paused through a resume, and the close is logged; and a connection closed for good readies nothing, while a running one lets out what it held. Its tests of a failed accounting and of a failed gate test the barrier, and go with it. anima-research#459's server-connection and registry changes, a refused policy's close fallback through the registry, and the rest of main merge without a conflict. In src/framework.ts, anima-research#454 meets this branch in initializeMcpl, where it reads the push coalescer's and checkpoints' state before anything is built, and anima-research#457 in stopOwned, where it ends ephemeral runs after the loop; neither touches what this branch changes there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two conflicts, both where main moved code this change also edits: - src/mcpl/checkpoint-manager.ts: main (anima-research#454) builds a feature set's tree and reads its host-managed state through loadHostState before holding it. This change's registration-first order is kept by that structure: registerFeatureSet takes main's form, and loadHostState registers the state's slot through ensureStateRegistered instead of main's catch-all, so a failed registration throws naming the state and leaves no tree, at registration and before a connect alike. - src/modules/workspace/index.ts: the import lines. main's observe.js names (anima-research#460) with this change's ensureStateRegistered import. After the merge every registerState call in src goes through ensureStateRegistered (17 sites, as before: main added none). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main had taken 24 merges since 86a3683, trains F and G among them: anima-research#480 (deferred writes reach disk), anima-research#481 (stop()'s budget bounds every wait), anima-research#457 (stop leaves nothing waiting), anima-research#454 (boot read errors), anima-research#424 (state registration errors), anima-research#412 (one MCPL tool resolver), anima-research#369 (silent drops say so), anima-research#429 (channel texts across servers) and anima-research#430 (one conversation spawn). Three conflicts: - src/framework.ts: both sides add an import at the same place, anima-research#247's inbound-source types and main's IncomingMessageFacts. Both are kept. anima-research#454 replaced initializePushCoalescer with restorePushCoalescer, which initializeMcpl now builds before anything else, and installPushCoalescer. anima-research#247's inbound-acceptance block stays just before it, as it stood before initializePushCoalescer, and anima-research#247's accepted hook lands in restorePushCoalescer's options unchanged. - src/mcpl/push-coalescer.ts: anima-research#247's accepted hook and main's new doc for the durability commit (one fsync per acceptance, under the lock). Both are kept. - src/mcpl/push-handler.ts: anima-research#247's InboundSource import, and main's isVisiblyEmptyConversion (anima-research#369) in place of isVisiblyEmptyContent, which push-handler no longer uses. One test needed the merge: anima-research#429's invitation test stands in a channel registry with the four methods main's push lane called. anima-research#247's push lane also names the channel in the message's source envelope, through getChannelLabel, which anima-research#247 added to the registry, so the stand-in gains it, reading the same labels as its getDescriptor. None of main's new lines reads or writes anima-research#247's turn silence, its cause, its drafts or its receipts. anima-research#457 ends a stopping framework's ephemeral runs in stop() and its cancelled provider retries through abortDroppedTurn, and has the event gate take a continuing turn's entry again (onInferenceResumed) without touching a pending self-wake. anima-research#481 bounds stop()'s waits and keeps a still-streaming agent's deferred writes queued. anima-research#247 reads none of these, and guardRetryTurnSilenced keeps to tool-result-guard retries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s 1 to 5 1. A registry that can't be read, or reads as another version, refuses the start and names the state (main's rule for the framework's own state, #454), where before it was taken as empty and overwritten by the first registration, with the results it held for delivery. 2. A failed registry write, ended-log append or sync fails the call that made it (#428's rule). deliverAgentResult fails on a failed sync and does not deliver; the result stays held for the next attempt. A hold whose persist fails leaves memory too, so a retry holds it once. 3. The registry and its ended log are recovery files beside the store (recovery/dendrite-registry.json, recovery/dendrite-ended.json), written through the atomic, fsynced path host mode and the deferred queue use; the file wins at boot over the branch-local state, which stays for inspection and for stores that predate the files. An undo's branch switch can't erase a held result or the record of work that ran. 4. reconcileAgentRegistry runs after host mode and the deferred-write queue are restored: a quiesced boot holds the restart notices and the redelivered mail as it holds any write, and held mail an entry of the restored queue carries is left to that entry's landing, so nothing lands twice under one mail id. 5. One teardown, releaseAgentRuntime, on every path a live agent ends by — stopAgent, the idle reap, an end that cascades: the window release, the gate's agentGone, the routing, prose and open-logical maps, the scripts, the provider-wait hold. A stop no longer drops a write queued for the agent at the next flush, and a failure notice it never read reaches the primary, as the reap always did. Tests shaped on nissa's probes (test/dendrite-durability.test.ts): each case asserts the fixed behaviour where the probe asserted the defect. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Restored state is no longer taken as empty at boot: it's refused where it can't be read, and read where it wasn't.
AgentFramework.create()now fails when a state it restores can't be read, or reads as something this version can't take for that state. The error names the state, or its recovery file, says it's unchanged, and says what to do. Before, each was taken as empty: the framework served without it, and the next write replaced what was stored, or a quiesced host booted serving. A host-managed MCPL feature set's state, which wasn't read after a restart, is now read before its server connects. A failed read of the inline caps, which loses nothing, is now logged.This applies #428's rule, that a failed read of the framework's own state isn't an empty state, to the boot reads #428's Not covered names, and to the reads of the agent settings, the tool-result guards and the MCPL checkpoint trees. It doesn't need #428 or #426 to be correct.
What was wrong
We ran each of these on
mainwith that one read failing as an I/O error would, or with the state in the form named. Those runs aren't part of this PR.readHostMode). A failed read ofrecovery/host-mode.json, or offramework/statewhen there's no file, read as "not quiesced". A host quiesced for maintenance booted serving, and the write it had deferred during the maintenance landed in the agent's context at that boot. A file holding something other than a host-mode record, such as["quiesced"], also read as not quiesced.restorePersistedDeferredWrites). A failed read ofrecovery/deferred-writes.json, or of the store's queue when there's no file, was logged and restored nothing. The next deferral replaced the file with itself alone, and resume landed only that one. When an agent's context couldn't be scanned for writes that had landed before their ack, the scan was logged and counted that context as holding none, so a write that had landed was restored and landed a second time at resume.existsSyncfirst, which answers false for any error, such asEACCESorENOTDIR, as for a missing file. So such a file read as absent, and the store's slot was read instead. That's by reading Node'sexistsSync; this PR's test of anENOTDIRhost-mode file showscreate()booting onmain's sources.restoreUsageState). A failed read started fresh totals, and the nextpersistUsageState, which each inference's completion calls, stored zeros over the stored ones.createAgent). A failed read was logged, and the agent booted at its recipe's settings. A stored{contextBudgetTokens: 60000, sameRoundThinkTextPolicy: "private"}booted at 100,000 andpublic, and the next settings change (tailTokens: 20000) stored{"tailTokens":20000}alone.initializePushCoalescer).mcpl/coalescing-recentlost a deferred push the framework had already acknowledged, and the next acceptance's receipt replaced the stored receipts.mcpl/coalescinglost a pending batch, and the next flush replaced the stored snapshot.adoptToolResultGuard). By reading: atoolResultGuardsthat isn't an object read as no setting, and the next guard change spread it and stored over it. Once any guard was stored, an agent namedconstructorreadObject.prototype.constructorthrough the record and failedcreate()as an invalid guard. This PR's test of that failscreate()onmain's sources.CheckpointManager.loadFromStore). A storedmcpl/checkpointswithouttreesread as no trees, and the next feature set registered stored over it. By reading: a failed read threw out ofinitializeMcplwith the registry and the other managers already assigned.registerFeatureSet). After a restart, a host-managed feature set's state wasn't read.loadFromStorerestores its tree without it, leaving it toregisterFeatureSet, which returns early for a tree it already holds. So the state read as empty, and the feature set's next delta checkpoint applied to nothing and stored over it. Onmain, a stored{count: 1}read asundefinedonce the store was opened again and the feature set registered, and a patch adding/otherthen stored{"other":2}.readPersistedToolResultInlineCaps). A failed read left no caps until a restart, with nothing logged. Setting another agent's cap kept the stored one, so nothing was lost.message_delta's output count as reported, with no default (src/providers/bedrock.ts:826). A delta without one made the totalsNaN, which JSON stores asnull. As this PR's test shows onmain's sources, a boot then started fresh totals.Changes
Each of these reads now restores what's stored, starts empty when nothing is stored, or throws an error that names the state. Nothing stored means no recovery file, no key in
framework/state, or a state the store holds nothing for; anullstored in a file or under a key isn't that. Entries inside a value these reads take, such as one write in the queue or one agent's usage, are checked as before.Recovery files: only a missing file means none was written. Any other error reading one throws, and text that isn't JSON refuses as a form this code doesn't read.
Host mode: a record that can't be read, or that isn't an object whose
quiescedistrueorfalse, throws. It's read before the deferred writes and before MCPL starts, so no deferred write lands and no data plane opens.Deferred writes: a queue that can't be read, or that is neither a list nor an object whose
pendingis a list, throws. So does an agent's context that can't be scanned for writes that already landed.Usage totals: totals that can't be read, or that aren't an object whose totals are token counts and whose
byAgent, if any, is a list, throw. A per-agent entry that isn't an object is skipped, as an invalid one already was.usage-tracker.tsexportspersistedUsageProblem, so the boot and the tracker agree on what restores. A count stored asnullreads as 0, in the totals and in each agent's entry, and a count that isn't a finite number adds 0. The cost is handled as before.framework/statethat isn't an object throws at its first boot read, the usage totals'.An agent's persisted runtime settings: a record that can't be read, or that isn't an object, throws from
createAgent, and so does a map of agents' settings that isn't an object. A record with an invalid field is handled field by field, as before.Tool-result guards: a
toolResultGuardsthat can't be read, or that isn't an object, throws when an agent is created: at boot, and, by reading, later too, since each way an agent is made adopts the guards. An agent's setting is read by its own name. A setting that isn't a boolean throws, as before.MCPL coalescing, checkpoint trees and host-managed state:
initializeMcplreads, checks and restores both coalescing states, then builds the checkpoint manager, which loads the trees, and reads each configured server's stored host-managed states, before it builds anything else. Any of these throws:treesis an object;At boot, that fails
create()before any MCPL server connects. With no MCPL servers configured, the coalescing and tree reads run at the firstconnectMcplServer(), which fails instead with nothing of MCPL built, so the next connect reads again.Host-managed MCPL state:
loadHostStatesreads every stored host-managed tree of a server with no state in memory, whatever its feature sets declare now. BesidesinitializeMcpl's call for each configured server, a runtimeconnectMcplServer()calls it before the server is added, so a read that fails leaves the server unconnected; MCPL stays up, even when that connect brought it up. A connect of a server already connected doesn't read:addServerrefuses it, as before. A tree whose state is in memory keeps it.A feature set with no stored tree, registering for the first time or after a disconnect removed its tree, is read when it registers, once its server has connected, as on
main.registerFeatureSetnow holds its tree only once that read succeeds, so the state is read again when the feature set next registers. By reading, the failed read fails that server's connect: at boot,create()logs that connect's failure and goes on, as for any server's failed connect, and the connection stays registered, as after any failed registration, so a plainconnectMcplServer()of it is refused until it's disconnected; a reconnect or a restart registers it again.The inline caps: a failed read is logged with the state, and boot goes on as before. A stored entry that isn't a valid cap is dropped and logged, as before, and so is one that
Number()can't convert, such as{"toString":null}.The texts, as an operator reads them, for a read that fails:
the host mode (<storePath>/recovery/host-mode.json) could not be read: <reason>. The framework doesn't start without it, since a host quiesced for maintenance would boot serving. The file is unchanged, and the framework starts once it can be read.the deferred context writes (<storePath>/recovery/deferred-writes.json) could not be read: <reason>. The framework doesn't start without them, since the next deferral would replace the stored queue. The file is unchanged, and the framework starts once it can be read.the deferred context writes (<path>) can't be checked for writes that already landed: <agent>'s context could not be read: <reason>. The framework doesn't start without that check, since a write that landed before its ack would land a second time. The queue is unchanged, and the framework starts once that context can be read.the usage totals (framework/state, usage) could not be read: <reason>. The framework doesn't start without them, since the next inference would store fresh totals over them. They're unchanged, and the framework starts once they can be read.the persisted settings of agent "<name>" (framework/state, agentRuntimeSettings) can't be restored: <reason>. The framework doesn't start without them, since the agent would boot at its recipe's settings and the next settings change would store over them. They're unchanged, and the framework starts once they can be read.the coalescing receipts since the last snapshot (mcpl/coalescing-recent) could not be read: <reason>. MCPL doesn't start without them, and neither does create() with MCPL servers configured, nor a first connectMcplServer(), since the next coalesced push or message would replace them. They're unchanged, and MCPL starts once they can be read.the coalescing receipts since the last snapshot (mcpl/coalescing-recent) can't be checked for occurrences already delivered: <agent>'s context could not be read: <reason>. MCPL doesn't start without that check, [...] since an occurrence delivered before the restart would be delivered again. They're unchanged, and MCPL starts once that context can be read.the MCPL checkpoint trees (mcpl/checkpoints) could not be read: <reason>. MCPL doesn't start without them, and neither does create() with MCPL servers configured, nor a first connectMcplServer(), since the next feature set registered would store over them. They're unchanged, and MCPL starts once they can be read.the host-managed state of feature set "<feature set>" on MCPL server "<server>" (mcpl/state/<server>/<feature set>) could not be read: <reason>. Neither create() with the server configured nor a connectMcplServer() of it goes on without it, since the feature set's next checkpoint would store over it. It's unchanged, and they go on once it can be read.[...] The feature set isn't registered without it, since its next checkpoint would store over it, so the server's connect fails. It's unchanged, and it's read again when the feature set next registers.the tool-result guards (framework/state, toolResultGuards) could not be read: <reason>. Agent "<name>" isn't created, since without them it would take its recipe's guard. They're unchanged, and it's created once they can be read.[agent-settings] the persisted tool_result_inline_max_chars caps (framework/state) could not be read: <reason>; they don't apply until a restart, and setting one keeps the others storedFor a form this code doesn't read, the text says what's there in place of
could not be read: <reason>, such asholds an array, not a host-mode record,holds text that isn't JSON: <the parser's reason>,hold an object whose pending is missing, not a list of writes,are a record whose totals aren't token counts,are a snapshot whose version is a string, not 1,are an object whose trees is missing, not checkpoint treesorare a string, not an object of agents' guards. For a recovery file, its last sentences name a record to write and say why removing the file doesn't help:The file is unchanged, and the framework starts once it holds a host-mode record, such as {"version":1,"quiesced":true} to boot quiesced or {"version":1,"quiesced":false} to serve. Removing it doesn't help: without the file, host mode is read from framework/state, which a quiesce doesn't write, so a quiesced host would boot serving.The queue's names{"version":1,"pending":[]}for none, and says removing the file reads the queue fromframework/deferred-writesinstead, which the file's writes don't keep, so it restores whatever older queue is there, or none. For a state in the store, it'sThey're unchanged, and starting again won't help until they're repaired.The guards' sayscreating it again won't help until the host's operator repairs them, since an agent is created at boot and later, by callers that can't repair them. A coalescing snapshot of another version saysThey're unchanged, and the version that wrote them reads them.What each part is for
The change is for one thing: a boot that can't read a state it will later write, or a quiesce it must honour, doesn't go on as if that state were empty, and a state it can read is read.
readRecoveryFiletaking onlyENOENTas no file closes the same gap one step earlier: an unreachable file isn't a missing one.wasPublished's: a context that can't be read can't say what was already delivered, and replaying it can land a message twice in an agent's context.initializeMcplbuilds anything leaves nothing built when either refuses, so after a refused runtime connect no guard sees MCPL as up.loadHostStates, at MCPL's start and before a runtime connect, is the rule's other half: a state the boot can read, and didn't, is read before any server can record a checkpoint over it, and a failure stops the boot or the connect as the other MCPL reads do. It finds a server's trees by their key prefix, since a tree is read whatever its feature set declares now, and only a connected server says what it declares. A runtime connect of a server already connected doesn't read at all, so its refusal staysaddServer's, and a tree whose state is in memory is left alone. Holding a first-time tree only once its state is read means a registration that fails is made whole by the next one, its stored tree and trace included.checkpoint-manager.ts'sthrownTextandstoredKindrender a thrown value and a stored value's kind in its refusals asframework.ts's helpers do. The repository keeps such helpers in each module, asmodule-registry.tsdoes.persistedUsageProblemkeeps the rule for what restores in the module that restores it.nullcount as 0, and adding 0 for a missing count, keep the totals a release could have stored from refusing the boot, and keep new ones from becomingNaN.StoredFormErrorlets a refusal say whether starting again can help where a failed read and a form problem come from one call:readAgentRuntimeSettings's read and its form checks, andreadRecoveryFile's read and its parse.jsonKindnames what a form check found without quoting its content.Number()can't convert from failingcreate()with an unnamedTypeError, so it's dropped like any other invalid entry.constructor, and a control that nothing stored starts empty.create()before the server sees aninitialize, and the next boot reads it, though the fixture's feature set is declared with neither flag. At a runtime connect, it fails the connect before the server is added, and the next connect reads it. On aCheckpointManagerover a real store opened again,loadHostStatesreads a stored tree's state, and its next patch applies to it. A state that can't be read failsloadHostStatesfor a stored tree, and the registration for a first-time one, each with its own text, until the next one reads it.loadHostStatesreads only its own server's host-managed trees, and not one whose state is in memory. A connect of a server already connected is refused as already registered, without a read.agent-runtime-settings.test.ts: its "a record the boot can't read is logged, and the agent boots at the recipe's" turns round.test/helpers/failed-create.ts: checks that a build fails with the message a test expects, and stops a build that succeeds, so its store is released.Not taken:
create()and closing an owned store, as fix(state): a failed read or write of our own state is an error, not an empty state or a success #428 does for the router's counters. Since fix(create): a failed create() releases what it had acquired #426, any failedcreate()releases what it acquired.StoredFormError, since nothing else branches on one.create()for a configured one.registerFeatureSetalready holds. Every connect reads its server's trees first, and a feature set declared with neither flag never registers, so that read would guard nothing.What callers will notice
AgentFramework.create()rejects, naming the state, where it booted before. That happens after one of these reads failed or found a form it doesn't read. A host restarted by a supervisor gets through a transient failure at its next start. Since fix(create): a failed create() releases what it had acquired #426, a failedcreate()releases the store it opened, so a retry in the same process can boot on it.connectMcplServer()rejects the same way, when the coalescing state or the checkpoint trees can't be taken. This applies on a framework that started with no MCPL servers, and the next call reads them again.create()fails with that server configured, and a runtimeconnectMcplServer()rejects before the server is added. By reading, a feature set with no stored tree fails that server's connect instead, whichcreate()logs and goes on from.create()as text that isn't JSON. Before, a host-mode file read as not quiesced, and a queue file was logged and restored nothing.create().main: the checkpoint trees in 0.1.0, the usage totals in 0.2.0, the agent settings in 0.6.6, the host mode and the deferred writes in 0.16.0, and the tool-result guards and the coalescing state in 0.20.0. The coalescing snapshots are version 1 throughout. A usage count stored asnullis the one form a release could leave that the totals' check would otherwise refuse, and it reads as 0. So no form check fires on a store a release left.Evidence
On Linux with Node 24.22.0, and chronicle 0.4.0, context-manager 0.14.1 and membrane 0.5.87 as
package.json's ranges install them:test/boot-state-read-errors.test.tshas 32 tests, on real stores and recovery files.getStateJsonthat throws for one state while one reader is on the stack. A recovery-file read fails throughreadRecoveryFile, or withENOTDIRfrom a file where its directory should be. A context read fails through the context manager'sgetMessageCountorgetAllMessages.create(), and the file is unchanged. The next boot is quiesced, with its deferred write still waiting for resume.null, text that isn't JSON, a stringquiescedand a missing one.create(), and the next boot is quiesced. A slot holding a string ornullfails too.create(). The next boot restores the write, and resume lands it once.create(), and the next boot that reaches it restores the write.create(). The next boot skips it, and resume leaves one copy.create()and stay stored, and the next boot restores 1,000 and 500.nullcount restore it as 0, and so does an agent's entry, and abyAgentholdingnullrestores.framework/statethat isn't an object failscreate(), unchanged.create(), and the next boot restores the setting. Three forms fail, each named and unchanged.create(). The next boot answers a retry with the original receipt and delivers the acknowledged work.create(), and the next boot renders its batch.create(), and the next boot delivers once.create(), and three forms fail, each named and unchanged.create(), naming it, before the server sees aninitialize, and stays as stored. The next boot reads it, though the fixture declares the feature set with neither flag.CheckpointManagerover a real store opened again,loadHostStatesreads a stored tree's state back as{count: 1}, and its next patch stores{count: 1, other: 2}.loadHostStatesfor a stored tree, and the registration for a first-time one, each with its own text, and the next one reads it. The first-time tree is then in the stored trees.loadHostStatesreads only its own server's host-managed tree, not another server's or a rollback-only tree, and a second call reads nothing once the state is in memory.constructorboots with settings and a guard stored for another agent, and has neither.agent-runtime-settings.test.ts: a record the boot can't read failscreate(), naming it, and the next boot restores its budget. A record that's an array ornull, and a map that isn't an object, fail, named.usage-tracker.test.ts: a usage without an output count, or withNaNcache counts, adds nothing for them, and the totals are stored as numbers.stop-with-host-operations.test.ts: the MCPLstop()tests' fake registry answersgetServerwithnull, as the registry does for an unknown server, since a runtime connect now asks it first.main's sources at86a36834, with the head's tests, 28 of the 32 new tests fail. Each fails with the error it gave on3adbdb86's sources, where this PR's earlier heads were checked.create(), the connect or the registration succeeded.create()threw the store's error unnamed.nullcount fails because the boot started fresh totals.CheckpointManagertests fail becausemainhas noloadHostStates. Onmain's sources, the tests' typecheck fails on those calls, and the build still emits them.constructoragent fails becausecreate()failed as an invalid guard.mainrefuses that connect as already registered too, since it reads nothing there; the test guards this change's own read.agent-runtime-settings.test.ts, the 3 new tests fail and its other 54 pass. Inusage-tracker.test.ts, the new test fails, because a missing output count made the stored totalnull, and the other 10 pass.ENOENTrule, the parse failure as a form problem, thenulland missing-count handling, theframework/statecheck, the settings' and the guards' own-key reads, both delivery checks, building the coalescer and the checkpoint manager first, reading restored host-managed states at MCPL's start and before a runtime connect, and only for a server not already connected,loadHostStates's three filters, the host-managed read's two refusals, holding a first-time tree only after its read, the store slot read when there's no file, the caps' log line and the catch around a cap's conversion, and each remedy text. Each ran where its code last changed: most at246073e0, the two recovery files' remedy texts atcb2a26c7, the conversion's catch ateea1d3f4, and the six in the deferred-write restore atddddcbc3, aftermain86a36834merged in.ddddcbc3,boot-state-read-errorspasses 32 of 32. Ate836b469, whose source and other test files the head keeps, so do 17 files beside it:agent-runtime-settings57 of 57,usage-tracker11 of 11,host-quiesce44 of 44,stop-with-host-operations58 of 58,failed-create-releases11 of 11,deferred-window-only29 of 29,mcpl-coalescing67 of 67,tool-result-guard126 of 126,tool-result-spill18 of 18,shutdown-errors28 of 28,mcpl-reconnect-reregistration4 of 4,placed-reaction-baseline49 of 49,present-while-acting57 of 57,checkpoint-attribution10 of 10,mcpl-runtime-lifecycle11 of 11,operator-surgery27 of 27 andephemeral-settle14 of 14.Not covered
:. A tree's key is<server>:<feature set>, so a server id with a:can share keys with another server's trees.loadHostStatesfinds a server's trees by that prefix, asremoveServeralready does.NaN, from a usage without a count, still makes the session's costNaN, stored asnull, and a currency mismatch still drops the cost until the next priced inference starts it again, as before. How the session's cost should read once an inference's cost is unknown is its own question.Written by Hilda-2681 (Claude Opus 5.5). Ezra-2423 (Claude Opus 5.5) reviewed its code at
eea1d3f4, and the merge ofmainand the two commits after it atddddcbc3.