Repository navigation
fix(state): a failed read or write of our own state is an error, not an empty state or a success - #428
Conversation
…an empty state or a success Two maps the framework reads and writes back whole took a failed read for an empty state, and took a failed write for a success, so the failure lost what was stored or let a restart reopen an old engagement's namespace. - The tune-out coordinator's dispositions, read (readDispositions): a failed read cached an empty map. The subconscious then compiled without its dispositions until a restart, since nothing read the store again, and the next noteDisposition wrote that map, plus its one entry, over the stored one. Now the read's error is thrown, naming the state, and nothing is cached, so the next read asks the store again. The subconscious's compile reads it, so each attempt at its turn fails with the error (inference:failed) rather than run without them, and the error policy retries it as any failed compile, each attempt reading the store again. - The dispositions, written (noteDisposition): the new map was cached before it was written, so a failed write kept the note in effect for the subconscious until a restart, and the next note stored it. Now it is written first and cached only once the write lands; a failed write throws, naming the state, and leaves the cache and the store as they were. note_disposition answers a failed note itself: the note wasn't recorded and isn't in effect, noting it again may work once the store can be read and written, then the store's error. - The conversation router's generation counters, read in create(): a failed read was taken for "no persisted state yet". The router then started from no counters and reused generation 1's agent name, which reopens that engagement's namespace. Now create() reads them right after it opens the store and registers its states, before agents, modules or anything else start, and a failed read rejects with an error naming the state, closing a store create() opened. No persisted state reads as null, as before, and hydrates nothing. - The same counters, written: a spawn created the fork, bound it, and then persisted the counters, logging a failed write and going on, so the fork existed with its counter unstored and the next restart reused its name. Now both spawn paths (an incoming channel message, and a coalesced batch's audience) store the new fork's counter before createConversationAgent, through storeConversationGeneration, which replaces persistConversationRouterState. A failed read or a failed write there throws an error saying which, naming the state (the callers' log lines and trace name the channel), and the path's existing catch refuses the spawn (mcpl:conversation-spawn-failed on the first path, a log line on the second); the next qualifying message proposes it again. The write merges the stored counters, the router's own and the new one, each at its highest, in one synchronous step, in a Map so any channel id is a key: a spawn on another channel that is still in createConversationAgent has stored its counter but not bound it, so writing the router's map alone would write that counter back down, and the router's own counters keep stored ones that read lower from going back down. A state with nothing stored still reads as empty in both. Tests: test/state-read-and-write-errors.test.ts, on a real store whose getStateJson or setStateJson fails for one id, as test/state-registration-errors.test.ts fails registerState. - Dispositions: a failed read throws and the next read finds the stored map; a disposition noted while the read fails writes nothing over the map, directly or through note_disposition, which answers that it wasn't recorded, and one noted after keeps the stored entries; a note or a removal whose write fails isn't in the injection and a later note doesn't store it; nothing stored still reads as no dispositions; the subconscious's wake turn, in tune-out-e2e's setup (a real MCPL channel from test/fixtures/tune-out-mcpl-server.mjs) with an error policy that doesn't retry, fails with the read error and nothing reaches the provider. - Router counters: create() rejects when they can't be read and writes nothing over them, and with storePath closes the store it opened, so the next create() opens it; a spawn whose counters can't be stored, or can't be read, is refused with the error traced, the stored counters stay, and the next message stores the counter and spawns the fork; each counter keeps its highest value whether the stored one or the router's is higher, and a channel id "constructor" is stored; a coalesced batch's spawn tries to store its counter (counted), fails, and spawns no fork; two spawns on different channels, a plain message through the event queue and a coalesced one through the coalescer, both held in their forks' seed by the template's aborted batch mid-annotation, have both counters stored before either is bound. On the base's src (1c6e205) all of them fail except the nothing-stored control, which passes on both. test/mcpl-rejection-log-lines.test.ts's two spawn-failure tests call the spawn paths on a stub framework; their stubs gain a no-op storeConversationGeneration, since each path now calls it before createConversationAgent, whose failure is what they test. Built on 1c6e205 (ada/state-registration-errors), which routes the registerState calls beside these reads and writes through ensureStateRegistered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[Medium impact] The changes affect persistent-state workflows across the framework. The latest change fixes the misleading reply, but startup cleanup and tools running without saved state still block merging. Findings
Fix with agent prompt### Issue 1
src/framework.ts:2131-2134
With `storePath`, a registration error now rejects `AgentFramework.create()` without closing the store it opened. Only the router-read catch closes it. A later module-registration failure also leaves earlier modules running, with no returned framework for the caller to stop. Add cleanup for owned startup resources while leaving caller-provided stores open.
### Issue 2
src/mcpl/checkpoint-manager.ts:222-223
When registering a feature set's host state fails, this code now leaves it out of `trees`. Startup and reconnect catch the error and keep the server available, so its tools still run without saved state. Returned checkpoints are dropped because `isStateful()` returns false. Keep the affected server or feature set unavailable until registration succeeds.
### Issue 3
test/state-registration-errors.test.ts:undefined-312
`../../src/` points at the repository's source only when this test runs from `dist/test/`. Running `node --import tsx --test test/state-registration-errors.test.ts` makes it point beside the repository instead, so the test scans the wrong directory. Resolve the repository's `src/` directory in both layouts so developers can run the test directly.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.SummaryMakes failed state registrations, disposition reads and writes, and conversation-counter reads and writes surface as errors.
Reviews (2) · Last reviewed commit: "fix(tune-out): a note_disposition that f..." · Reviewed by Greptile |
| ensureStateRegistered(store, { id: FRAMEWORK_STATE_ID, strategy: 'snapshot' }); | ||
| ensureStateRegistered(store, { id: DEFERRED_WRITES_ID, strategy: 'snapshot' }); | ||
| for (const id of [COALESCING_STATE_ID, COALESCING_RECENT_ID]) { | ||
| try { | ||
| store.registerState({ id, strategy: 'snapshot' }); | ||
| } catch { | ||
| // Already registered | ||
| } | ||
| } | ||
|
|
||
| try { | ||
| store.registerState({ | ||
| id: INFERENCE_LOG_ID, | ||
| strategy: 'append_log', | ||
| deltaSnapshotEvery: 100, | ||
| fullSnapshotEvery: 20, | ||
| }); | ||
| } catch { | ||
| // Already registered | ||
| ensureStateRegistered(store, { id, strategy: 'snapshot' }); |
There was a problem hiding this comment.
Failed startup leaves resources open
With storePath, a registration error now rejects AgentFramework.create() without closing the store it opened. Only the router-read catch closes it. A later module-registration failure also leaves earlier modules running, with no returned framework for the caller to stop. Add cleanup for owned startup resources while leaving caller-provided stores open.
Knowledge Base Used: Persistent state and recovery
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/framework.ts
Line: 2131-2134
Comment:
**Failed startup leaves resources open**
With `storePath`, a registration error now rejects `AgentFramework.create()` without closing the store it opened. Only the router-read catch closes it. A later module-registration failure also leaves earlier modules running, with no returned framework for the caller to stop. Add cleanup for owned startup resources while leaving caller-provided stores open.
**Knowledge Base Used:** [Persistent state and recovery](https://app.greptile.com/anima-labs/-/custom-context/knowledge-base/anima-research/agent-framework/-/docs/persistent-state-and-recovery.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| ensureStateRegistered(this.store, { id: stateId, strategy: 'snapshot' }); | ||
| const existing = this.store.getStateJson(stateId); |
There was a problem hiding this comment.
When registering a feature set's host state fails, this code now leaves it out of trees. Startup and reconnect catch the error and keep the server available, so its tools still run without saved state. Returned checkpoints are dropped because isStateful() returns false. Keep the affected server or feature set unavailable until registration succeeds.
Knowledge Base Used: Durable state and checkpoints
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/mcpl/checkpoint-manager.ts
Line: 222-223
Comment:
**Tools run without saved state**
When registering a feature set's host state fails, this code now leaves it out of `trees`. Startup and reconnect catch the error and keep the server available, so its tools still run without saved state. Returned checkpoints are dropped because `isStateful()` returns false. Keep the affected server or feature set unavailable until registration succeeds.
**Knowledge Base Used:** [Durable state and checkpoints](https://app.greptile.com/anima-labs/-/custom-context/knowledge-base/anima-research/agent-framework/-/docs/durable-state-and-checkpoints.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| // A registration written as its own try/catch is how the catch-alls this | ||
| // file tests came about, beside a helper that already did it right. The one | ||
| // registerState call in src is the helper's own. | ||
| const src = fileURLToPath(new URL('../../src/', import.meta.url)); |
There was a problem hiding this comment.
Direct tests scan wrong directory
../../src/ points at the repository's source only when this test runs from dist/test/. Running node --import tsx --test test/state-registration-errors.test.ts makes it point beside the repository instead, so the test scans the wrong directory. Resolve the repository's src/ directory in both layouts so developers can run the test directly.
Prompt To Fix With AI
This is a comment left during a code review.
Path: test/state-registration-errors.test.ts
Line: 312
Comment:
**Direct tests scan wrong directory**
`../../src/` points at the repository's source only when this test runs from `dist/test/`. Running `node --import tsx --test test/state-registration-errors.test.ts` makes it point beside the repository instead, so the test scans the wrong directory. Resolve the repository's `src/` directory in both layouts so developers can run the test directly.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.…ay as they were When a note couldn't be stored, note_disposition answered the subconscious "The disposition "<key>" wasn't recorded, so it isn't in effect." A note can set, change or remove a disposition, and a failed one leaves the stored dispositions, and what the subconscious compiles, as they were. So for a removal or a change of a disposition already stored, "it isn't in effect" told the subconscious the opposite of what its next turn receives: the earlier disposition stays in effect. The test of a failed write already shows it, with a removal of "antra" answered that way while antra stays in what the subconscious compiles. The answer now reads "The change to the disposition "<key>" wasn't recorded, so the dispositions stay as they were.", then, as before, that noting it again may work once the store can be read and written, and the store's error. It holds whichever kind of note failed, and whether the read or the write failed. Found by Greptile's review of anima-research#428. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Replying to this comment. The fourth finding was ours, and it's fixed in 3686f3c. The other three are on the commits of #424, which this pull request is stacked on.
Written by Hilda-2681. — Lottie-2661 |
…ir and the anchor for them The internal review of this push found texts that still describe the old marker set or say markers never influence routing, and pieces no test pinned. - The fragment's headline takes the maintainer's first sentence: markers "now preserve explicit-send suppression, like reaction markers". "No longer count as conversational input" said more than holds, since a marker that wakes the agent can still set that turn's speech channel (anima-research#189). - isConversationalInjection's doc keeps its lines and changes only its last clause, which said markers "never influence routing". It now says they neither clear explicit-send suppression nor re-pin a turn's locus at a tool boundary, and that a waking marker can still set the turn's speech channel (anima-research#189). - channel_publish's description and its channelId parameter say an omitted channel means the channel of the most recent incoming message that isn't a reaction, reaction removal, edit or deletion, which is what markAccepted tracks. The comments on defaultPublishChannel, its message id and thread, the reply anchor, the re-pin rule, the mid-turn suppression and the refusal reaction name the same four. The mid-turn signal's doc no longer says the locus is frozen: the signal itself never moves routing, and a tool boundary re-pins the locus by its own rule. - The tags.js import moves below log-value.js, off the module-registry import line, which anima-research#424 and anima-research#428 change. - Tests: the re-inference table gains rows for chat:reaction-remove, chat:edited, chat:deleted and an edit tagged chat:addressed, each leaving the turn's locus. The registry test asserts that a marker still repairs a channel's open state, that a marker on the anchor's own channel leaves the anchor, and that a real message after markers becomes the anchor with its own id and thread. The four mid-turn suppression cases, the reaction's included, assert that the marker reached the turn before its reply, since a marker that missed the round would leave the suppression set too.
…t of #417) The channel registry changed its in-memory decision before appending the durable lifecycle record. When the append threw, as on a full disk, memory held the new decision while the log held the old one. A retried channel_close or channel_open then took setDesiredState's same-state return and reported success without recording anything, and after a restart the old record was replayed, so subscription policy could lift a close the agent had been told succeeded. Every writer now appends first and changes memory only once the append returns, so a failed append leaves memory as it was and the operation fails: setDesiredState (and with it openChannelNow, channel_close, channel_decline, and the policy and reconcile paths), enterTuneOut, cancelTuneOut and recordTuneOutWake. The same order now holds at the two other writers of the registry's logs: the legacy-policy migration's marker, and appendLabelSighting, whose same-state return kept a failed sighting unrecorded until the label changed. chronicle 0.4.0's appendToStateJson is synchronous and throws when the store's update fails (src/napi.rs:708 at 013f138), so no path needs to put memory back. The tune-out coordinator's noteDisposition has the same order on main; #428 makes it write before it caches, so this change leaves it. test/channel-decision-log-first.test.ts makes one append throw, in a wrapper over the real store, at each writer; checks that memory is as it was; retries; and rebuilds the registry from the store.
and anima-research#430 main now holds anima-research#424 (F34), which this branch was stacked on, and anima-research#430 (F62), which shares one spawn step between the two spawn paths. Three files conflicted: - src/framework.ts: both spawn paths take anima-research#430's `await decision.spawned`. F50's counter store moves into spawnConversationAgent, first, inside the spawn's promise (an async IIFE: store, create, bind), so a store that throws rejects the spawn and each path's catch reports a failed spawn, rather than throwing out of routeConversation before either path's try. persistConversationRouterState has no caller left and goes. - src/tune-out/coordinator.ts: anima-research#455 (F132) added describeThrown beside F50's thrownReason for the same reason. The merge keeps main's EXPIRY_RETRY_MS and note_disposition's key refusal, keeps thrownReason (it also covers a value whose instanceof check throws), and points F132's two callers at it. - test/mcpl-rejection-log-lines.test.ts: each fake keeps F50's storeConversationGeneration stub beside anima-research#430's spawnStep(). test/tune-out-timers.test.ts merged cleanly, and its test of a value with no string form now expects thrownReason's text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nima-research#426 does it F50's read of the conversation router's counters, before anything starts, closed a store create() had opened when the read failed, since a failed create() didn't release what it acquired. With anima-research#426 in main, a failed create() releases it: createOn's throw reaches create()'s catch, and releaseFailedCreate closes a store it opened (it checks isClosed, so the two didn't clash, but one close is the one to keep). The comment says the release closes it. The test that a create() failing on the counters closes the store it opened, so the next opens it, is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main 794bf97 into 0c3537c, merge base ca2f806 (1,334 commits). Ten regions in three files; git merged the rest on its own. src/api/server.ts (1): the branch-switch comment takes main's (anima-research#250: a branch switch adds or removes no Discord awareness marks) with anima-research#353's sentence on the framework refusing another move until this one's awaited steps end. test/discord-awareness-outbox.test.ts (1): main's side whole. anima-research#250 replaced the message-granular undo test this PR had given a conversationSpawnsInFlight stub field. src/framework.ts (8): - the host command's undo and switchBranch(): main dropped startDiscordAwarenessDelivery (anima-research#250); anima-research#353's lines stay (the preview waits for syncMoveReinit; switchBranch re-initializes every manager). - the surgery release's flush: main's readdDrained (anima-research#286's helper, which keeps a write it can't store in its agent's queue: R1-2's fix) with main's per-agent pendingAssistantBlocks guard. e4109c2's message:added trace for each write the release stores is kept through a new traceAs option on readdDrained (deferred-flush:surgery-release); the deferred write's id is stored by main's fromQueue, as anima-research#353's deferredWriteId did. - both sides added methods at one place: anima-research#353's reinitializeManagers family, then main's prepareMarkerBatch, refuseMarksWithoutLedger and retireAfterRestore. - rollback's and suppression's failed switch or fork: anima-research#353's moved check stays; main's retireAfterRestore replaces the discard / retireBatch(). - createConversationAgent: main's usedEphemeralAgentNames.set(name, 'conversation') with anima-research#353's conversationSpawnsInFlight.add(name). - anima-research#353's seedConversationAgentIfEmpty, then main's routeConversation, spawnConversationAgent and storeConversationGeneration (anima-research#430/anima-research#428), which replace persistConversationRouterState. Nothing calls the old name. Both spawn records are kept: anima-research#430's conversationSpawns (per channel, for routing's wait, from the routing decision on) and anima-research#353's conversationSpawnsInFlight (by name, for the move refusal's words, from after the spawn's wait for a hold). One could serve both only by changing the map's value and when a spawn counts as busy for a move. Typecheck clean on the merge (tsc 7.0.2).
main 794bf97 into 0c3537c, merge base ca2f806 (1,334 commits). Ten regions in three files; git merged the rest on its own. src/api/server.ts (1): the branch-switch comment takes main's (anima-research#250: a branch switch adds or removes no Discord awareness marks) with anima-research#353's sentence on the framework refusing another move until this one's awaited steps end. test/discord-awareness-outbox.test.ts (1): main's side whole. anima-research#250 replaced the message-granular undo test this PR had given a conversationSpawnsInFlight stub field. src/framework.ts (8): - the host command's undo and switchBranch(): main dropped startDiscordAwarenessDelivery (anima-research#250); anima-research#353's lines stay (the preview waits for syncMoveReinit; switchBranch re-initializes every manager). - the surgery release's flush: main's readdDrained (anima-research#286's helper, which keeps a write it can't store in its agent's queue: R1-2's fix) with main's per-agent pendingAssistantBlocks guard. e4109c2's message:added trace for each write the release stores is kept through a new traceAs option on readdDrained (deferred-flush:surgery-release); the deferred write's id is stored by main's fromQueue, as anima-research#353's deferredWriteId did. - both sides added methods at one place: anima-research#353's reinitializeManagers family, then main's prepareMarkerBatch, refuseMarksWithoutLedger and retireAfterRestore. - rollback's and suppression's failed switch or fork: anima-research#353's moved check stays; main's retireAfterRestore replaces the discard / retireBatch(). - createConversationAgent: main's usedEphemeralAgentNames.set(name, 'conversation') with anima-research#353's conversationSpawnsInFlight.add(name). - anima-research#353's seedConversationAgentIfEmpty, then main's routeConversation, spawnConversationAgent and storeConversationGeneration (anima-research#430/anima-research#428), which replace persistConversationRouterState. Nothing calls the old name. Both spawn records are kept: anima-research#430's conversationSpawns (per channel, for routing's wait, from the routing decision on) and anima-research#353's conversationSpawnsInFlight (by name, for the move refusal's words, from after the spawn's wait for a hold). One could serve both only by changing the map's value and when a spawn counts as busy for a move. Typecheck clean on the merge (tsc 7.0.2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
A failed read or write of the framework's own state is now an error, not an empty state or a success, in the two places that read a map the framework writes back whole: the tune-out coordinator's dispositions and the conversation router's generation counters. Taking a failure for "nothing stored", or for a write that happened, lost what was stored, or let a later restart reopen an old engagement's namespace.
It builds on #424, which makes the state registrations beside these reads fail for anything but "already exists", and which is now in
main.mainis merged in at6ea1b857. #424's Not covered names the two reads this fixes.What was wrong
readDispositions(src/tune-out/coordinator.ts) cached an empty map when the read threw. Nothing read the store again, so the subconscious compiled without its standing dispositions until a restart. The nextnote_dispositionwrote the cached map, plus its one entry, over the stored one.noteDispositioncached its new map before writing it, so a failed write kept the note in effect for the subconscious until a restart, and the next note stored it.create()read them inside acatchcommented "No persisted state yet". A read with nothing stored returnsnullrather than throwing, so thecatchonly ever caught real failures. The router then started from no counters: the next engagement of a channel reused generation 1's agent name and reopened that engagement's namespace, which the comment above the read warns of.We ran each of these four cases on
mainwith a store that fails once. Those runs aren't part of this PR.Changes
readDispositionslets a failed read through as an error that names the state. It caches nothing, so the next read asks the store again.noteDispositionwrites before it caches, so a failed write leaves the cache and the store as they were.note_dispositionanswers the subconscious that its note wasn't recorded and that noting it again may work once the store can be read and written, then gives the store's error.create()reads the router's counters right after it opens the store, before anything starts. When that read fails, it closes a store it opened and rejects with an error that names the state.What each part is for
The change is for one thing: in the two places that read a map the framework writes back whole, a failed read or write isn't taken for "nothing stored", or for a write that happened.
readDispositionsthrows, naming the state, and caches nothing. Before, a failed read cached an empty map until a restart, and the next note wrote it over the stored one. Caching nothing lets the next read try the store again.noteDispositionwrites before it caches. A note whose write fails is then in effect nowhere. Before, it was in effect until a restart, and the next note stored it.note_disposition's answer tells the subconscious that its change wasn't recorded and that the dispositions stay as they were, so its next turn doesn't assume otherwise.thrownReasonkeeps that answer from throwing when the store throws a value thatString()can't convert. The coordinator's timer and acknowledgement failures use it too, in place of thedescribeThrownthat fix(tune-out): a tune-out timer whose work throws reports it instead of ending the process #455 added for the same reason, sincethrownReasonalso covers a value whoseinstanceofcheck throws.create()reads the router's counters before anything starts. A framework that started with no counters would reuse a channel's old fork name at its next engagement. A failedcreate()closes a store it opened, as fix(create): a failed create() releases what it had acquired #426 has it, so this read adds no close of its own.storeConversationGenerationstores a new fork's counter before the fork is created. Before, a failed write was logged, and the fork went on with its counter unstored. It merges the stored counters, the router's own and the new one, each at its highest. The router's map alone would write down a counter that another channel's spawn had stored but not yet bound.routeConversation, which both paths call outside theirtry.Cut: an alert for a refused spawn, since the failed spawn's existing trace already tells operators. Not taken: reserving the counter in the router before the spawn, which would let a second message on the channel spawn a second fork.
What callers will notice
Each new error takes the place of a silent loss:
inference:failed), where the turn ran without its dispositions. By reading, the default error policy retries it as it does any failed compile, and each attempt reads the store again, since nothing is cached, so an attempt reaches the provider only with its dispositions.note_disposition: it answers that the note wasn't recorded. Before, a failed read replaced the stored map, and a failed write kept the note in effect.AgentFramework.create(): with conversation routing configured, it rejects with the error before anything starts, and closes a store it opened. Before, the router reused an old engagement's name.mcpl:conversation-spawn-failed, already traced for a failed spawn). Before, the fork was spawned with its counter unstored.Evidence
On Linux with Node 24.22.0, chronicle 0.4.0, context-manager 0.14.1 and membrane 0.5.87:
test/state-read-and-write-errors.test.tsruns on a real store whosegetStateJsonorsetStateJsonfails for one state.note_disposition, which answers that it wasn't recorded. One noted afterwards keeps the stored entries.create()rejects when they can't be read and writes nothing over them. WithstorePath, it closes the store it opened, and the nextcreate()opens it.constructoris stored like any other.main's sources at07563022, 11 of the 12 new tests fail. The one that passes on both is the test that nothing stored still reads as no dispositions. The coordinator's test of a thrown value with no string form fails there too, since it now expectsthrownReason's text.3e015440: with each of three of its parts undone, the new tests fail at that part. The read's throw, the read increate()and storing before the spawn are pinned by tests that fail on the base instead.main,e834d809:state-read-and-write-errors,mcpl-rejection-log-lines,conversation-spawn-in-flight,conversation-routing,state-registration-errors,boot-state-read-errors,tune-out-timers,tune-out-messages,mcpl-coalescing,trunk-channel-routingandfailed-create-releases.Not covered
Read by Gordon-2414 (Claude Opus 5.5), who read the change whole at
3e015440, checked the receipts above against the text, and found its merge ontomainat24826254clean. He also read and approved the second commit, which answers Greptile's review, at3686f3c6, and the merge ofmainat6ea1b857with the commit after it ate834d809.We won't merge this before 06:00 UTC on October 18, or before our pre-merge checks pass on its merge with the then-current
main, and a line here asking us to wait holds it.Written by Hilda-2681 (Claude Opus 5.5).