Skip to content

fix(boot): restored state that can't be read or taken fails create(), naming it, not an empty state - #454

Merged
nissa-seru merged 12 commits into
anima-research:mainfrom
nissa-seru:hilda-2681/boot-read-errors
Oct 18, 2026
Merged

nissa-seru merged 12 commits into
anima-research:mainfrom
nissa-seru:hilda-2681/boot-read-errors

Conversation

@nissa-seru

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

Copy link
Copy Markdown
Contributor

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 main with 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.

  • Host mode (readHostMode). A failed read of recovery/host-mode.json, or of framework/state when 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.
  • Deferred writes (restorePersistedDeferredWrites). A failed read of recovery/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.
  • A recovery file that can't be reached. Both reads asked existsSync first, which answers false for any error, such as EACCES or ENOTDIR, 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's existsSync; this PR's test of an ENOTDIR host-mode file shows create() booting on main's sources.
  • Usage totals (restoreUsageState). A failed read started fresh totals, and the next persistUsageState, which each inference's completion calls, stored zeros over the stored ones.
  • An agent's persisted runtime settings (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 and public, and the next settings change (tailTokens: 20000) stored {"tailTokens":20000} alone.
  • MCPL coalescing (initializePushCoalescer).
    • A failed read of mcpl/coalescing-recent lost a deferred push the framework had already acknowledged, and the next acceptance's receipt replaced the stored receipts.
    • A failed read of mcpl/coalescing lost a pending batch, and the next flush replaced the stored snapshot.
    • Both were logged as "starting with unknown history".
    • A snapshot of a version this code doesn't read was ignored, and the next flush replaced it with a version 1 snapshot without its batch.
    • By reading: when an agent's context couldn't be read to check whether a restored occurrence had been delivered, it counted as not delivered, so the occurrence was delivered again.
  • Tool-result guards (adoptToolResultGuard). By reading: a toolResultGuards that 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 named constructor read Object.prototype.constructor through the record and failed create() as an invalid guard. This PR's test of that fails create() on main's sources.
  • MCPL checkpoint trees (CheckpointManager.loadFromStore). A stored mcpl/checkpoints without trees read as no trees, and the next feature set registered stored over it. By reading: a failed read threw out of initializeMcpl with the registry and the other managers already assigned.
  • Host-managed MCPL state (registerFeatureSet). After a restart, a host-managed feature set's state wasn't read. loadFromStore restores its tree without it, leaving it to registerFeatureSet, 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. On main, a stored {count: 1} read as undefined once the store was opened again and the feature set registered, and a patch adding /other then stored {"other":2}.
  • The inline caps (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.
  • Usage counts that a provider leaves out. By reading membrane 0.5.87: its Bedrock stream takes a message_delta's output count as reported, with no default (src/providers/bedrock.ts:826). A delta without one made the totals NaN, which JSON stores as null. As this PR's test shows on main'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; a null stored 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 quiesced is true or false, 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 pending is 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.ts exports persistedUsageProblem, so the boot and the tracker agree on what restores. A count stored as null reads 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/state that 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 toolResultGuards that 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: initializeMcpl reads, 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:

    • either coalescing state can't be read;
    • the snapshot isn't version 1, or the receipts aren't a list;
    • the coalescer can't restore either one;
    • an agent's context can't be read to check an occurrence's delivery;
    • the trees can't be read, or what's stored isn't an object whose trees is an object;
    • a configured server's stored host-managed state can't be read.

    At boot, that fails create() before any MCPL server connects. With no MCPL servers configured, the coalescing and tree reads run at the first connectMcplServer(), which fails instead with nothing of MCPL built, so the next connect reads again.

  • Host-managed MCPL state: loadHostStates reads every stored host-managed tree of a server with no state in memory, whatever its feature sets declare now. Besides initializeMcpl's call for each configured server, a runtime connectMcplServer() 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: addServer refuses 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. registerFeatureSet now 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 plain connectMcplServer() 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.
    • For a feature set with no stored tree, read when it registers: [...] 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.
  • Logged: [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 stored

For a form this code doesn't read, the text says what's there in place of could not be read: <reason>, such as holds 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 trees or are 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 from framework/deferred-writes instead, 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's They're unchanged, and starting again won't help until they're repaired. The guards' says creating 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 says They'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.

  • Each read's throw replaces a silent loss, or an ignored quiesce, with a refusal that names the state. Its cost: a host that meets a transient read failure at boot has to restart, where before it served without that state.
  • Each form check does the same for a value that reads but isn't one this code takes. By reading, no release writes such a form (below), so these fire only on a store something else changed.
  • readRecoveryFile taking only ENOENT as no file closes the same gap one step earlier: an unreachable file isn't a missing one.
  • The two delivery checks' throws, the landed-write scan's and 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.
  • Restoring the coalescer and loading the checkpoint trees before initializeMcpl builds 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 stays addServer'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.
  • The guards' read by the agent's own name, as the settings' is: an inherited property isn't a stored setting.
  • checkpoint-manager.ts's thrownText and storedKind render a thrown value and a stored value's kind in its refusals as framework.ts's helpers do. The repository keeps such helpers in each module, as module-registry.ts does.
  • persistedUsageProblem keeps the rule for what restores in the module that restores it.
  • Reading a null count 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 becoming NaN.
  • StoredFormError lets 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, and readRecoveryFile's read and its parse.
  • jsonKind names what a form check found without quoting its content.
  • The caps' log line tells an operator why a cap is missing. Nothing more is needed, since setting a cap reads the stored ones again.
  • The catch around a cap's conversion keeps one stored entry that Number() can't convert from failing create() with an unnamed TypeError, so it's dropped like any other invalid entry.
  • The tests:
    • For each site: a boot with that read failing, or with its state in a form this code doesn't read, fails naming it and leaves the state as it was. The next boot that can read the state restores it.
    • Also covered: an unreachable recovery file, recovery files that aren't JSON, the store slots without recovery files, the forms releases wrote, the caps' log line, the runtime connect, an agent named constructor, and a control that nothing stored starts empty.
    • Host-managed MCPL state: at boot, a configured server's stored state that can't be read fails create() before the server sees an initialize, 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 a CheckpointManager over a real store opened again, loadHostStates reads a stored tree's state, and its next patch applies to it. A state that can't be read fails loadHostStates for a stored tree, and the registration for a first-time one, each with its own text, until the next one reads it. loadHostStates reads 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:

  • Reading these early in 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 failed create() releases what it acquired.
  • Booting quiesced on an unreadable host-mode record. It would hold every data plane on a host that may never have been quiesced, with only silence to show it.
  • Error classes beyond StoredFormError, since nothing else branches on one.
  • Reading every stored tree's host-managed state when the checkpoint manager loads the trees. A state of a server no longer configured would then stop MCPL for every server, where reading by server stops only that server's connect, or create() for a configured one.
  • Reading a tree registerFeatureSet already 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.
  • Reading a feature set's state before its server connects when no tree is stored for it. Its server's declarations are known only once it has connected, and the read at registration keeps the state from being taken as empty, since a feature set whose read fails isn't registered.

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 failed create() 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.
  • A server with a host-managed feature set sees its stored state after a restart, and its next patch applies to it. When a stored tree's state can't be read, create() fails with that server configured, and a runtime connectMcplServer() rejects before the server is added. By reading, a feature set with no stored tree fails that server's connect instead, which create() logs and goes on from.
  • A recovery file left empty or torn, as a power loss can leave one (it's renamed into place without an fsync), now fails 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.
  • Creating an agent fails when the tool-result guards can't be read or aren't an object: at boot, and, by reading, later too, as for a conversation fork.
  • An agent's persisted settings: 0.22.2's notes say a record the boot can't read is logged, and the agent boots at the recipe's. It now fails create().
  • Stores from earlier releases: by reading, each release's writers of these states write only forms this code reads. That holds for every tag from the release each state first appears in through 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 as null is 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:

  • The new tests: test/boot-state-read-errors.test.ts has 32 tests, on real stores and recovery files.
    • How the failures are made: a store read fails through a getStateJson that throws for one state while one reader is on the stack. A recovery-file read fails through readRecoveryFile, or with ENOTDIR from a file where its directory should be. A context read fails through the context manager's getMessageCount or getAllMessages.
    • Host mode:
      • A file that can't be read, or can't be reached, fails create(), and the file is unchanged. The next boot is quiesced, with its deferred write still waiting for resume.
      • Five file forms fail, each named: an array, null, text that isn't JSON, a string quiesced and a missing one.
      • With no recovery file, a slot that can't be read fails create(), and the next boot is quiesced. A slot holding a string or null fails too.
    • Deferred writes:
      • A queue that can't be read fails create(). The next boot restores the write, and resume lands it once.
      • A queue file that can't be reached fails create(), and the next boot that reaches it restores the write.
      • Five file forms fail, each named. With no recovery file, the slot's failed read and a string in it fail too.
      • A write that landed before its ack, with its agent's context failing to scan, fails create(). The next boot skips it, and resume leaves one copy.
    • Usage totals:
      • Totals that can't be read fail create() and stay stored, and the next boot restores 1,000 and 500.
      • Four forms fail, each named.
      • Totals with a null count restore it as 0, and so does an agent's entry, and a byAgent holding null restores.
      • A framework/state that isn't an object fails create(), unchanged.
    • Inline caps: a failed read is logged in full, and boot goes on. Setting another agent's cap stores both. A stored cap that can't become a number is dropped and logged, and the other agent's cap applies.
    • Tool-result guards: a failed read fails create(), and the next boot restores the setting. Three forms fail, each named and unchanged.
    • Coalescing:
      • Receipts that can't be read fail create(). The next boot answers a retry with the original receipt and delivers the acknowledged work.
      • A snapshot that can't be read fails create(), and the next boot renders its batch.
      • A context that can't confirm a receipt's delivery fails create(), and the next boot delivers once.
      • Seven forms fail, each named and left as stored.
    • Checkpoint trees: trees that can't be read fail create(), and three forms fail, each named and unchanged.
    • Host-managed MCPL state:
      • At boot, a configured server's stored state that can't be read fails create(), naming it, before the server sees an initialize, and stays as stored. The next boot reads it, though the fixture declares the feature set with neither flag.
      • At a runtime connect, it fails the connect, and the server isn't added. The next connect adds it and reads the state.
      • On a CheckpointManager over a real store opened again, loadHostStates reads a stored tree's state back as {count: 1}, and its next patch stores {count: 1, other: 2}.
      • A state that can't be read fails loadHostStates for 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.
      • loadHostStates reads 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.
      • A connect of a server already connected is refused as already registered, though its tree with nothing stored would fail a read.
    • At a runtime connect: each coalescing state's or the trees' failed read fails the connect. Nothing of MCPL is built, the stored states are unchanged, and the next connect restores the receipt.
    • Controls: the forms releases wrote, and a store from before the recovery files, boot as before. A first boot with nothing stored starts serving.
    • An agent named constructor boots with settings and a guard stored for another agent, and has neither.
  • agent-runtime-settings.test.ts: a record the boot can't read fails create(), naming it, and the next boot restores its budget. A record that's an array or null, and a map that isn't an object, fail, named.
  • usage-tracker.test.ts: a usage without an output count, or with NaN cache counts, adds nothing for them, and the totals are stored as numbers.
  • stop-with-host-operations.test.ts: the MCPL stop() tests' fake registry answers getServer with null, as the registry does for an unknown server, since a runtime connect now asks it first.
  • Red-first: on main's sources at 86a36834, with the head's tests, 28 of the 32 new tests fail. Each fails with the error it gave on 3adbdb86's sources, where this PR's earlier heads were checked.
    • Most fail because create(), the connect or the registration succeeded.
    • The guards' and the trees' failed reads fail because create() threw the store's error unnamed.
    • The caps test fails because the caps' line isn't among the lines logged.
    • The cap that can't become a number fails because no line drops it.
    • The null count fails because the boot started fresh totals.
    • The three CheckpointManager tests fail because main has no loadHostStates. On main's sources, the tests' typecheck fails on those calls, and the build still emits them.
    • The constructor agent fails because create() failed as an invalid guard.
    • The 4 that pass on both are the 3 controls and the duplicate connect. main refuses that connect as already registered too, since it reads nothing there; the test guards this change's own read.
    • In agent-runtime-settings.test.ts, the 3 new tests fail and its other 54 pass. In usage-tracker.test.ts, the new test fails, because a missing output count made the stored total null, and the other 10 pass.
  • Removals: each of the change's 57 parts, taken out on its own and rebuilt, fails a test that pins it. They are each read's throw and each form check, the recovery files' ENOENT rule, the parse failure as a form problem, the null and missing-count handling, the framework/state check, 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 at 246073e0, the two recovery files' remedy texts at cb2a26c7, the conversion's catch at eea1d3f4, and the six in the deferred-write restore at ddddcbc3, after main 86a36834 merged in.
  • At the head: at ddddcbc3, boot-state-read-errors passes 32 of 32. At e836b469, whose source and other test files the head keeps, so do 17 files beside it: agent-runtime-settings 57 of 57, usage-tracker 11 of 11, host-quiesce 44 of 44, stop-with-host-operations 58 of 58, failed-create-releases 11 of 11, deferred-window-only 29 of 29, mcpl-coalescing 67 of 67, tool-result-guard 126 of 126, tool-result-spill 18 of 18, shutdown-errors 28 of 28, mcpl-reconnect-reregistration 4 of 4, placed-reaction-baseline 49 of 49, present-while-acting 57 of 57, checkpoint-attribution 10 of 10, mcpl-runtime-lifecycle 11 of 11, operator-surgery 27 of 27 and ephemeral-settle 14 of 14.
  • The whole suite is this PR's CI: typecheck, build and tests on Node 20 and 24, on Ubuntu and macOS.

Not covered

  • fix(state): a failed read or write of our own state is an error, not an empty state or a success #428's two reads, the tune-out dispositions and the router's counters, still take a value in a form they don't read as empty, as fix(state): a failed read or write of our own state is an error, not an empty state or a success #428's Not covered says.
  • A turn checkpoint's missing or unparseable blob reads as none, and the next save replaces it. That's a runtime read on another path.
  • The router's counters after a store moves to another branch aren't read again, as fix(state): a failed read or write of our own state is an error, not an empty state or a success #428 notes.
  • A recovery file that vanishes. With a recovery path, a missing file is read from the store's slot, as on a store from before the files, and the file's writes don't keep that slot. So a vanished file reads stale or empty state, as it did.
  • A feature set registration that throws, such as a first-time feature set's state that can't be read, fails its connect but leaves the connection in the registry, and the registrations after it aren't made, as before.
  • A stored tree keeps its stored flags when its server now declares the feature set differently, as before.
  • Server ids with a :. A tree's key is <server>:<feature set>, so a server id with a : can share keys with another server's trees. loadHostStates finds a server's trees by that prefix, as removeServer already does.
  • An unknown cost. A cost membrane computes as NaN, from a usage without a count, still makes the session's cost NaN, stored as null, 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 of main and the two commits after it at ddddcbc3.

Hilda-2681 and others added 7 commits October 17, 2026 14:21
…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>
@greptile-apps

greptile-apps Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5 Tier: apex

[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

  1. P2 Malformed caps can stop boot ▶
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.

Summary

This PR stops boot from treating unreadable required state as empty.

  • The framework keeps host mode and deferred writes intact when it boots.
  • Usage totals stay readable when saved or incoming counts are missing.
  • Agents keep their saved runtime settings and tool-result guards.
  • MCPL restores saved receipts and host-managed state before servers connect.

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 nissa-seru in the PR description:

Reviews (1) · Last reviewed commit: "fix(boot): the queue file's refusal says..." · Reviewed by Greptile

Comment thread src/framework.ts Outdated
Comment on lines +5818 to +5819
for (const [name, value] of Object.entries(data?.toolResultInlineCaps ?? {})) {
const n = Number(value);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Hilda-2681 and others added 5 commits October 17, 2026 14:57
… 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>
@nissa-seru
nissa-seru merged commit d57466a into anima-research:main Oct 18, 2026
6 checks passed
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 18, 2026
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>
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 18, 2026
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>
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 18, 2026
…-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>
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 18, 2026
…'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>
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 18, 2026
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>
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 18, 2026
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>
tel-0s added a commit that referenced this pull request Oct 23, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant