Repository navigation
feat(agent): declare later tool and system-text changes in the context - #690
nissa-seru wants to merge 95 commits into
Conversation
dbe2619 to
3274e6e
Compare
An agent with toolDeclarations: 'entries' sends a fixed head: the tools and system text recorded at its first request. Each later change to its tools, and each addition to its system text, goes into its context as a system change, stored where it took effect, which membrane renders as a mid-conversation system message. The provider then reads its cache through the change instead of rewriting it from the head. 'head', the default, sends requests as before. - system-changes.ts holds the pure part: folding changes onto a head, and the difference from what the agent advertises now. Tools are compared by name and canonical definition. System text is compared by blocks, as a multiset: new blocks are an addition, and anything else is a replacement. - After the committing compile, the agent adopts its current head when none is recorded, and moves the head to what's current for a replacement: it removes the changes the new head holds, then records a copy of it, so a stop between the two leaves no change beside a head that holds it, and a definition changed in place later is declared. Otherwise it declares the difference as one change at the end, except while its own last turn has calls with no results yet, wherever other participants' messages fall, and while the store's last message, past other changes, holds anyone's tool call, since a change there would stand between the call and its result: in the residents' shared slot it can be another resident's. kv-unified hashes the head that goes out, and the compile runs again after a move that needs it. A dry run shows the same, compiling again (dry) where the move would, and stores nothing. - On the head, change rows stay out of the request, and a record and rows left from an earlier period are removed, on whichever branch is current. The direct API path leaves change rows out too. A request's last message, and the last message host /undo reports, are read past change rows. - The framework keeps heads in its store's state, read once per branch: a branch switch reads them again. Entries it can't use are set aside and named in the log, and a read that fails is said once a branch. A closed fork's record goes with it. Residents share one message slot, so each resident's view, and the subconscious's, hides other agents' change rows, and a subconscious on another model than its principal declares on its head. A fork starts from its template's head and changes. - Readers of stored messages read past change rows: host /undo N counts the conversation's messages and reports them, and history's tools show that a change was made, never what it declared, since in the shared slot it may be another resident's. - isSystemChangeMessage is exported for a host's own readers. A toolDeclarations value it can't read is refused, naming its kind, never the value. - Traces: head:declared and head:moved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng compile Changes declared in the context are read on every request until they go into the head. Once they reach a tenth of the last response's reported input (its fresh tokens and its cache's reads and writes), priced from their JSON as context-manager prices them, the agent moves the head to what's current at the first committing compile whose firstChanged, from context-manager, is at or before the first change: that compile already rewrites every change, so the move adds the head and any messages before firstChanged, never more than the whole prompt. A strategy that keeps changes ahead of its window (the passthrough strategies, once over budget) re-renders from the first change only at the compile where its window first passes it; after that the change stays first and unchanged. firstChanged is null after a start or a branch switch, and nothing consolidates then. thinkingBinding.binds (default true) says the model binds its thinking to everything before it. Moving the head sets aside every thinking block in the request, and thinking from firstChanged on is already set aside by the compile's own change, so a binding model consolidates only where no thinking, redacted thinking included, sits before firstChanged, and the changes otherwise stay where they happened. The framework passes each response's reported input to the agent, and head:moved says a consolidation's tokens and the input they reached. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
If the provider refuses a request that carried a system change, the request goes again at once on the agent's current head, with no change rows, and the agent keeps to its head until it's built again. The refusal never reaches the error policy, for which a 400 is terminal; the resend is a retry of the same turn (attempt + 1), so it records no second undo checkpoint and keeps the turn's tool-call count, and a failure of the resend reaches the error policy as attempt 1. It carries no change, so it happens once per change. A model that binds its thinking resends only after the refusal membrane recognises (MembraneError.systemChangeRefused), since moving its head sets its thinking aside, and a 400 about something else shouldn't cost that. Another resends after any 400 from a request that carried a change. The trigger is at the stream's error event, where membrane reports a failed request, its own refusals before sending included. Trace: head:resend. The README's Agents section and a changelog fragment describe toolDeclarations and thinkingBinding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ying request A refusal in wording membrane doesn't recognise left a binding model's change stored, and every later request carrying it failed the same way, until something moved the agent's head. Now any model, binding or not, resends on its head after any 400 from a request that carried a change. On a binding model that sets aside the thinking the request held, the cost a replacement or a consolidation already has, once per change, since the resend carries none. A 400 membrane doesn't recognise as a refused system change also raises an operator line (ops:alert, kind head-resend) quoting it, so the cost shows and membrane can learn the wording. The line says the 400 may not be about the change, that a binding model's thinking was set aside, and that the agent keeps to its head until a restart, so each change until then rewrites its cache. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the input, or over budget Consolidation waits for a compile that already rewrites the changes, and on a model that binds its thinking for one with no thinking before firstChanged. Under the strategies as built that compile may never come: the autobiographical head window keeps its first replies' thinking, and a passthrough window passes the first change before the changes reach a tenth. Every request then carried every change for good, and under kv-unified the kept changes ended in OverBudgetError. So once the changes reach a fifth of the last response's input with no free compile having taken them, the head moves at the committing compile anyway (forced: 'share'): one whole write, and on a binding model the thinking the request held set aside, once per crossing. And where a budget leaves the changes less room than a fifth, a committing compile refused over budget while this agent's change rows are kept moves the head and compiles once more (forced: 'budget'); if that compile is refused too, the changes weren't the cause and the error goes on as before. Neither runs in a dry run. head:moved says which, and an operator line (ops:alert, kind head-consolidated) gives the sizes and the cost. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A host that forks a subagent seeds it with its parent's messages. Under
toolDeclarations: 'entries', the child had no recorded head, so it
adopted its current head at its first request: a whole write of its
context, with any copied change rows sent again after a head that already
held them.
createEphemeralAgent now takes { headFrom }, naming a running agent whose
recorded head the new agent starts with, as a copy in its own store.
systemChangeMetadata is exported beside isSystemChangeMessage, so the
host stamps the change rows it copies as the child's. The child's first
request then carries the parent's prefix and declares only its own
difference. A parent on its head, or with nothing recorded, gives
nothing, and the child adopts as before; a name that isn't running is
refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…em changes context-manager 0.22.0 is on npm with anima-research/context-manager#298. A message holding only membrane's `system_change` blocks changes the tools and system text the model is offered from its place on. Every strategy compiles it unaltered, in its place among the raw messages, and the autobiographical strategy keeps it raw, as a pin keeps a message. The declarations in this pull request write such messages, so they need 0.22.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3274e6e to
73d9b82
Compare
There was a problem hiding this comment.
Automated review
Recommendation: needs a maintainer's decision. The change belongs, but part of it needs a maintainer's call.
Reviewed commit 73d9b82 with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.
Does this change belong in the project?
It fits, with concerns. An opt-in way to keep the cached head fixed and declare later tool and system changes as membrane system changes belongs in this framework. Maintainers should still decide two things: the breaking context-manager ^0.22.0 floor, which every host must follow, and the separate createEphemeralAgent headFrom API that rides along with it.
Findings
2 findings of medium or high severity, see the inline comments:
- Medium severity. Change rows are recognised by their block but removed by their metadata, so a row without this agent's metadata is never consolidated away.
- Medium severity. Every head write snapshots all agents' full tool definitions and system text into the store.
2 findings about lines the diff does not show:
- Medium severity. isNewestIn skips clock rows but not system change rows.
src/framework.ts:24349-24350 - Medium severity. The fail-safe covers only stream error events, not a stream that throws.
src/framework.ts:21690-21709
Details of these 2 findings
- Medium severity,
src/framework.ts:24349-24350. isNewestIn skips clock rows but not system change rows. Like a clock row, a system change row isn't a message anyone sent, and the PR makes other tail readers (the[Continue]logic, host undo, the last message the agent sees) look past change rows.isNewestInstill takes the newest row that isn't a clock row fromqueryMessages({limit:2, reverse:true}). When a change row is stored after a channel message, for example by another resident's assembly in the shared slot, that message no longer counts as newest. The batched wake line that #727 leaves out is then written anyway. It should skipisSystemChangeMessagerows the same way. - Medium severity,
src/framework.ts:21690-21709. The fail-safe covers only stream error events, not a stream that throws.keepToHeadAfterRefusalis called only in the error-event branch (line 21252). The catch path that logs 'Stream threw' and gives up never checks for a refused system change. If asystemChangeRefusederror or a 400 for a request carrying a change arrives as a throw rather than an error event, the change stays stored and the agent stays on 'entries'. Every later request then carries the same change and fails the same way, which is exactly the loop the fail-safe exists to prevent. The tests only send refusals as error events (RefusedStream).
5 more findings, less severe
- Low severity, documentation,
README.md:94-97. README says a refusal moves the head, but the fail-safe actually turns 'entries' off and fires on any 400. The README says "a refusal of a change move[s] the head to what's current, with one write". The code does something different.keepToHeadAfterRefusal(src/framework.ts) callskeepToHeadUntilRestart, and the next assembly then takes the!entriesbranch ofsettleHead, which deletes the recorded head and every one of the agent's change rows. That runs for any HTTP 400 on a request that carried a change, whatever caused the 400, not only for a recognised refusal. The AgentConfig doc in src/types/agent.ts states this correctly. The README should say the same, so operators know an unrelated 400 (an oversized image, say) switches the agent off 'entries' until restart. - Low severity, test coverage,
src/framework.ts:12315-12342. No test covers the unreadable or malformed head-record paths.readHeadDeclarationshas three branches. It drops entries that aren't heads, it drops a stored record that isn't a map, and whengetStateJsonthrows it returns null, so no write happens and every agent sends its current head. None of these is tested (searching test/ for the 'set aside' log finds nothing). A regression there could make one agent's adoption overwrite the other agents' heads, or loop on adoption. Add tests with a corrupt record and with a store whose read throws. - Low severity, documentation,
src/agent.ts:1592-1594. Comment about compiling against context-manager 0.20.0 is stale now the floor is ^0.22.0. The edited line still saysrenderStatsis read structurally "so this compiles against 0.20.0 too". This PR raises the dependency in package.json to ^0.22.0, so that compatibility reason no longer applies and the comment misleads a reader. - Low severity, maintainability,
src/framework.ts:13873-13874. Fork records its template's head object without copyHead. Everywhere else a recorded head goes throughcopyHead, which system-changes.ts says is so a definition changed in place can't alter a head unseen. The fork path writes the template's ownHeadDeclarationobject under the fork's name, so two map entries share one object. Nothing mutates it today, but this undermines the invariantcopyHeadexists to keep. UsecopyHead(templateHead). - Low severity, maintainability,
src/framework.ts:992-1005. indexBeforeLastMessages reimplements the tail-window scan in stored-removal.ts. The new helper walksgetMessageWindowbackwards in fixed blob-free windows.locateMessageIndicesin src/stored-removal.ts already does the same thing with its own window constant. A shared tail-scan helper would keep the window size and the blob handling in one place.
| /** Remove this agent's change rows from its context: by their metadata, | ||
| * which in the residents' shared slot names the agent they declare for. */ | ||
| private removeOwnChanges(): number { | ||
| const rows = this.contextManager.queryMessages({ metadata: { kind: 'system-change', agent: this.name } }).messages; | ||
| for (const row of rows) this.contextManager.removeMessage(row.id); | ||
| return rows.length; | ||
| } |
There was a problem hiding this comment.
Medium severity. Change rows are recognised by their block but removed by their metadata, so a row without this agent's metadata is never consolidated away.
isSystemChangeMessage recognises a change by its system_change block "whatever metadata a stored copy carries". settleHead therefore folds every such row in the compile into effectiveDeclaration and prices it in consolidation. removeOwnChanges, though, deletes only rows whose metadata is {kind:'system-change', agent:this.name}. A change row with no metadata or the wrong agent keeps showing up in compiled messages after moveHead writes a head that already contains it. Examples are a host seeding an ephemeral headFrom agent without systemChangeMetadata, or a row written by an older build. That row then goes out beside a head that already holds it. Under 'entries', its price can trigger a forced consolidation at every request, with a full cache write each time. Under 'head', the 'on-head' trace fires on every request with changesRemoved: 0. Removal should match how rows are recognised, or rows without this agent's metadata should be ignored consistently.
| private storeHeadDeclarations(change: (map: Map<string, HeadDeclaration>) => void): void { | ||
| const map = this.readHeadDeclarations(); | ||
| if (!map) return; | ||
| change(map); | ||
| try { | ||
| this.store.setStateJson(HEAD_DECLARATIONS_ID, Object.fromEntries(map)); | ||
| } catch (error) { | ||
| console.error('[head-declarations] the recorded heads could not be stored:', error); | ||
| } | ||
| } |
There was a problem hiding this comment.
Medium severity. Every head write snapshots all agents' full tool definitions and system text into the store.
storeHeadDeclarations calls setStateJson(HEAD_DECLARATIONS_ID, Object.fromEntries(map)), registered with strategy 'snapshot'. This writes every agent's complete tool schemas and system prompt blocks on each adoption, replacement, consolidation and delete. A system-position injection whose text changes between requests (a status line, say) is a 'replace' at every request, so it causes a head move and a full-map snapshot each time. Store growth then scales with requests × agents × prompt size. Nothing caps or dedups these writes, and no test checks how much they write.
A change row was recognised by its block wherever it came from, so settleHead folded every change row the compile showed onto the head, and consolidation priced them all, while a move of the head removed only the rows stored with the agent's own metadata. A change row a host stored without that metadata, as in a context it seeds for an ephemeral agent, was then folded and priced but never removed: under 'entries' its price could force a consolidation at every request, each a whole cache write, and on the head every request traced an 'on-head' move that removed nothing. ownSystemChanges (system-changes.ts) finds the agent's own change rows in a compile: a change by its block, compiled from stored rows that each carry the agent's change metadata, the rows removeOwnChanges takes. A compile's messages carry no metadata, so it's read from the rows the compile names as each message's sources (its provenance and rawBodies); a compile that names none has none of the agent's own. settleHead folds only these onto the head, consolidation prices them and takes the first of them as the first change, a dry run's move of the head leaves only them out, and on the head only they set off the removal and its trace. A host's change row is an ordinary message to all of these: under 'entries' it goes out as stored, the agent declares its own difference without counting it, and no consolidation takes it. What a request leaves out on the head, and what readers of the conversation's tail look past, stay every change row. The toolDeclarations and headFrom docs say so. consolidation reads CompileResult.firstChanged as context-manager 0.22.0 types it. Tests: ownSystemChanges by the metadata of each message's source rows (not another agent's, a host's, a summary, a mix, or rows the compile didn't read; none without sources); an agent with a host's change row and one of its own declares nothing for the host's, prices no consolidation from it, and a replacement, as its dry run shows first, takes only its own row; a host's row whose change the agent's current tools still need is declared again; a consolidation's first change is the agent's own first, past a host's row before it; on the head a host's row traces nothing and stays. The fake context's compile names each row as its message's source, as a compile's provenance does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kip a write that changes nothing The recorded heads were one map in one snapshot state, so every write, an adoption, a replacement, a consolidation or a delete, stored every agent's whole head: its tool definitions and its system text. A system injection whose text changes at every request makes every request a replacement, and so a snapshot of every agent's head. Each agent's head is now in a state of its own, `framework/head-declarations/<name>`, so a write covers that agent alone, and a write that would store what the state already holds isn't made. Each state is registered before its first read or write, since a branch made from another, as an undo makes one, carries registered states only. A head is read once per branch and kept in memory, as before. A record that isn't a head is set aside, and the agent adopts again. A state that can't be read leaves that agent sending its current head, said once a branch, and its next write replaces the record; with one state per agent, that write can't reach another agent's head. Deleting a head records none (null). The record is unreleased, so its layout changes with no migration. A conversation agent forked from a template records a copy of the template's head (copyHead), as every other recorded head is, rather than the template's own object. Tests: one agent's replacement writes its own state alone and leaves another's as it was; a write of the head already recorded isn't made, and one of another head is; a record that isn't a head is set aside with one line, the agent adopts and the record is a head again, another agent's untouched; while a state can't be read the agent sends its current head, said once, and its record is written again; a fork's head is a copy, not its template's object. An undo's branch keeps the head from before the move, as the branch-switch test already asks, which a state left unregistered would lose. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nges; one tail scan for the stored-row lookups isNewestIn, which decides whether a batched wake's line is left out because its message is still the newest entry, took the newest row that isn't a clock row from the last two stored. A system change, which any resident's assembly stores in the shared slot, isn't a message a reply could be meant for either, and the other readers of the tail already look past it; stored after a channel message, it made the wake's line be written anyway. And two rows reached past one clock row at most. scanFromTail (stored-removal.ts) visits the branch's rows newest first, in blob-free windows, until its caller is done. isNewestIn now looks past every clock row and every system change, however many, with it. locateMessageIndices and indexBeforeLastMessages (host undo by messages) each had a tail scan of their own, with a window of their own; both use it now, with stored-removal's window. Tests: a DM stays newest with change rows (the agent's, another agent's and one unstamped) and clock rows after it, and isn't once a message follows them; scanFromTail visits every row newest first with its index across window boundaries without blobs, stops where its visit is done, and reads nothing in an empty store; locateMessageIndices finds ids across windows and stops reading once all are found. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s context-manager 0.22.0 types them The README and the changelog fragment said a refusal of a change moves the head to what's current. What the fail-safe does is send the request again on the agent's current head and keep the agent to its head until it's built again, its record and change rows going at the next assembly, and it does so for any 400 that ends a request carrying a change, as well as for a refusal membrane recognises. Both now say that, the README with the consequence that an unrelated 400, such as one for an oversized image, ends the agent's 'entries' too, as AgentConfig's doc already said. With context-manager 0.20.0 out of the range, `CompileResult.renderStats` is read as typed, and what was kept for 0.20.0, whose compiles carry no render stats, goes: the structural read and its comment, a sentence of `getRequestCompileStats`' doc, the render-stats test's branch that only 0.20.0 ran, and the `request-compile-stats` fragment's sentence that agent-framework's range stays `^0.20.0`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…osed fork's head, and start the tail scan small Readers of this round found three costs it hadn't named. - A head's state was registered at its first read, and every request on the head reads one to delete a leftover record, so every agent on 'head', the default, every conversation fork included, registered a state for a feature that's off. A registration lasts for good, and a branch made from another walks every registered state. A state is now registered just before its first write. A state never registered reads as null on chronicle 0.4.0 (by run), and holds nothing an undo's branch must carry, so every record is still registered before its data exists. - Closing a conversation fork deleted its head, for a reason that went with the shared map: "the map is written whole at every head write". A fork's name is never used again, so its head now stays, as the rest of its data does. - isNewestIn read the tail scan's first window of 500 rows before finding the newest, which is nearly always within the first few: about 0.8 to 1.3 ms against 0.05 to 0.4 ms for its old read, on 1,000 to 20,000 rows (by run). The scan's first window is now 16 rows, about 0.03 ms, and each next window doubles, up to 500. Tests: an agent on its head registers no state and reads no record, and an agent under 'entries' registers its own at its adoption; a closed fork's head stays; the tail scan's windows start at 16 rows and double to 500, and a reader done with the newest row reads one window of 16. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ions Conflicts in src/agent.ts and src/types/agent.ts, where anima-research#785 and this branch both changed the activation's assembly and `thinkingBinding.binds`. Resolved as a union, with this branch's reads of `binds` left as they were (the next commit gives the key one reading): - the imports: both sets; - `headHash` (this branch) and `wireDigest` (anima-research#785): both kept; - assertAgentSwitches' doc: anima-research#785's, with this branch's `toolDeclarations` clause; the `binds` refusal's words: anima-research#785's; - the assembly: this branch's head, budget fallback and settleHead, with anima-research#785's binding options passed to each compile, and anima-research#785's `compiled` (messages, provenance, `thinkingRecordedAfter`) read from the compile the request is built from (settled.compiled), not the first; - the `binds` doc: anima-research#785's paragraph, then this branch's on consolidation. One test met anima-research#785 without a conflict: the operator line for a forced share set `binds: true` so that a stored reply's thinking would stand before the change. With anima-research#785, `binds: true` turns on context-manager 0.22.0's record, and a reply stored before the branch's record began goes without its thinking at the compile (ThinkingStrip's cause 'first', without the drop control), so the consolidation was free and nothing was forced. The test now leaves `binds` out, which this branch reads as a model that may bind while the compile keeps the thinking. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d a binding compile told the head it's made for With anima-research#785 on main, two features read `thinkingBinding.binds`, each from its own safe side: consolidation counted a model as binding unless `binds` was false, and anima-research#785's record, notes, budget rule and alert came on only with `binds: true`. The key now has one reading in three states, with a predicate for each side beside the config check (agent.ts), used by both: - `mayBindThinking` (`binds` isn't false): what a choice that could set kept thinking aside reads, which is consolidation's. The Agent's `mayBindThinking` replaces this branch's `bindsThinking`. - `declaresBinding` (`binds` is true): what relies on the declaration reads, which is anima-research#785's: thinkingBindingOption, and through it each compile's binding, the notes, the budget rule and the alert. Left out, `binds` declares nothing, and each feature takes its own safe side. The type's doc says so in one paragraph, and so does the refusal of a `binds` that isn't a boolean. A compile with `binds: true` is told digests of what the request carries ahead of its messages (`identity`). Under 'entries' the request carries the recorded head, not what the agent advertises now, so the digests are now the head's that each compile is made for, as kv-unified's hash is: bindingCompileOptions takes a head, the assembly's compile is told the binding of the head it's given, and the stream notes with the binding of the head the request goes out on. A declared change keeps the digests, so the next compile keeps the thinking before it. A move of the head changes them, so the compile for the new head leaves out every thinking block, the cost of a move on a binding model. Before this, every declared change read as a change of head to the record, which would have left out all thinking at each one. A move compiles again for the new head whenever the compile was told anything of the old one, kv-unified's hash or the binding's digests, which are both of the head's tools and system text, as headHash compares them. The direct API's compile is told its current head, as before. The operator lines that name the thinking a move or a resend sets aside say it outright with `binds: true`, as a condition with `binds` left out ("if its model binds it"), and not at all with `binds: false`. Tests: the three states as the Agent reads them; under 'entries' with `binds: true`, each compile's digests are those of the system text and tools the request carries, kept by a declared change and changed by a move, a replacement with no change row to take included, and the stream's binding is the compile's the request was built from; on context-manager 0.22.0 with `binds: true` and the drop control, a declared change keeps a reply's thinking before it, and a replacement and a forced consolidation each leave it out at the compile for the new head, with the next request built; the forced move's operator line names the thinking outright with `binds: true` and as a condition with `binds` left out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it from the compile, price it as context-manager does, and shed the agent's own change rows over budget in every state
- Where a change may go. The guard read only the last stored row that isn't a change, and only for a call. context-manager 0.22.0 refuses a change written at the end by its own rule (MessageStore.refuseToolPairSplit): back over change rows and rows of results, each body read whole across its shards, to the nearest body holding a call, each of whose calls must be answered by the body's own later blocks or by a result read on the way. So the guard let through a call on a body's first shard, or a parallel call another result left partly answered, and addMessage then threw; and it held back a cycle its own blocks answer, so requests kept the old tools until another row came. changeWriteCheck (system-changes.ts) states the rule over stored rows read newest first, and the agent reads them from the store's tail in blob-free windows (scanFromTail), as far as the rule reads, rather than the whole branch with every attachment inflated (getAllMessages).
- A declared change in the compile. A change the assembly declared went out appended after the committing compile. context-manager's next compile compares with the last committing one, which didn't hold it, so its firstChanged fell at the change: where that was the agent's first change, a consolidation at a tenth read as free and moved the head, rewriting what the provider had cached. The compile now runs again once the change is stored, so the request carries it as the compile renders it, and context-manager's last committing compile holds it (and, with `binds: true`, the record the thinking binding compares with). Refused over budget, that compile's error goes on with the change stored, for the next assembly's budget fallback. A dry run appends the change as before.
- The price. Changes were priced at JSON length / 4, where context-manager prices a system_change block at JSON length / 2.3 (jsonTokenEstimator), so both thresholds fired about 1.7 times late. They're priced with context-manager's own estimator.
- Over budget. A compile refused over budget with the agent's own change rows kept moved the head only under 'entries' with a head recorded. On the head, whose requests leave the rows out, and under 'entries' before a head is recorded, rows that no longer fit refused every compile, since the removal and the adoption that would take them follow a compile. Now in every state the rows come out and the compile runs once more: on the head as settleHead's removal ('on-head'), with no record as its adoption ('adopted'), and with one as the move forced by the budget. If that compile is refused too, the error goes on and the move stands, which the comment now says ("as before" it said).
- removeOwnChanges takes a row only if it holds a change, as ownSystemChanges reads one, so a message stored with change metadata that holds none stays, as a dry run shows it.
- A dry move leaves the agent's own change rows out of the compile's sources as it does out of its messages, so the two stay index for index.
- ownSystemChanges lost a check of an empty source list, which its check of the rows already covers.
- settleHead's doc: a dry compile has no firstChanged, so a preview shows a consolidation only once one is forced; and on the head, a record the store can't read stays until a read succeeds.
Tests: changeWriteCheck's cases, among them a call last or past change rows, parallel calls with one answered and with each answered, a cycle answered by its own blocks and one left with an open call, a call on a body's first shard, a shard that starts another body, a message of another kind after the call, an older call past an answered one, and an empty store; an agent under 'entries' waits after another's partly answered parallel call and after a call sharded over a body, and declares after a cycle its own blocks answer; on the head and before a record, a compile refused over budget with the agent's own rows takes them out and compiles once more, and under kv-unified the compile after a move forced by the budget hashes the head that goes out; a declared change is read as sent at the next compile, so no free consolidation follows it, by a stand-in whose firstChanged compares with its last committing compile; a change at the very start consolidates; firstChanged isn't read below a tenth; the thresholds by context-manager's estimator; a dry move's sources stay index for index with its messages; a move takes only rows holding a change; an ownership case whose row names the agent but isn't marked a change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rent's head after the child's context opens, and pass over unstamped change rows in the shed - The recorded heads were cached by branch name, so a branch made under the name of one deleted before it could read that branch's cached head. They're cached by the branch's id, and so is the once-a-branch log of a record that can't be read. - createEphemeralAgent read headFrom's head before awaiting ContextManager.open, while the host copies the parent's messages once the call returns, so a move of the parent's head between the two left the child's head and its copied change rows relative to different heads. The head is read after the open. - The poisoned-history shed passed over rows marked system only, so a change row a host stored without metadata was shed as if it were a turn. It passes over every change row, as the tail readers do. - The operator line for a move forced by the changes' share said no compile had rewritten them; on a model that may bind, a compile can rewrite them and still not take them for free, since thinking sits before them. It says no compile had taken them into the head at no further cost. - storeHeadDeclaration's comment said the store keeps the old head until a later write lands; a later write of the same head is skipped, so it's until a write of another head lands, or a restart or a branch switch reads the store again. Tests: each agent's record is read once a branch; a branch reads its own record though one of the same name came before it; a child gets a copy of its parent's head, so a change made in place to what the parent's request carries doesn't reach it; the child takes the parent's head as it stands once its context is open; the shed passes over an unstamped change row; the operator line for a move forced by the budget, and the line for a forced share naming no thinking for a model whose `binds` is false and the thinking on a condition with `binds` left out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…es the changes into the head too The compile that now runs once a declared change is stored counts the change, so it can be refused over budget where the first compile fitted, as with a compile near its budget and a change defining tools with their schemas. Only the first compile had the fallback, so the refusal went up through the stream's start: an [inference-failed] marker in the agent's context, the compile-refused alert and a lost turn, and then the next assembly's fallback made the same move anyway. That compile now has the same fallback: the agent's own change rows, the new one included, go into the head, and the compile runs once more for it, a move forced by the budget. The fallback is one helper both compiles use (takeChangesOverBudget). The change guard reads the context manager's tail without checking for getMessageCount and getMessageWindow first, which context-manager ^0.22.0 always has. Tests: the compile after a declared change refused over budget, which the head takes the change into, with the head recorded and the move traced as forced by the budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… rows out, carry a copy of the head in each request, and compile a move again for any head told differently - On the head, the committing compile held the agent's own change rows from an earlier 'entries' period, which the request leaves out and the assembly then removes. With `binds: true`, context-manager's record of that compile kept them, so the next compile, without them, differed from it there and left out the thinking minted since: after the fail-safe's resend, the resend's reply's. Once rows are removed, the compile now runs again without them, so what it commits holds what the request carries. - A request carried the recorded head's tool objects themselves, so a hook or a caller that changed what the request carries in place changed the cached record, and the next request declared a change back. A request now carries a copy. A record is never changed in place, so the copies a fork's record and a headFrom child's record were made from no longer pay for themselves, and a fork's or child's record may be its template's or parent's object. - A move compiled again only when kv-unified's hash of the old and new heads differed, but the hash writes a field left undefined as null, where the binding's digests leave it out as the wire does: a dry move between heads that differ only so was compiled with the old head's digests. A move now compiles again when anything the compile is told of the head differs (toldAlike), and the doc says a dry move can't remove rows before it compiles, so under a tight budget its window can hold less than the move's. - A closed conversation fork's head stays in the store, but leaves the in-memory cache, which otherwise kept every retired fork's head on a long-lived branch. - Text: keepToHeadAfterRefusal's doc sits above it again, with thinkingSetAside's before it; the console line and the head-resend operator line say the agent keeps to its head until it's built again, as the rest of the text does; the README says a free consolidation keeps no thinking before the first message the compile renders anew (`firstChanged`), which is the rule; jsonTokenEstimator's import moves to the top. Tests: on context-manager 0.22.0 with `binds: true` and the drop control, an agent built on its head after an 'entries' period takes its change row out and compiles again, and a reply it stores and notes after keeps its thinking at the next compile; a stream's note under the compile a declared change's request was built from is taken; a recorded head stays as recorded when what a request carries is changed in place; a dry move between heads told differently only by a field left undefined compiles again; the change guard reads one blob-free window of the newest rows of a long store; after a second refusal over budget, the move stands; a closed fork's head leaves memory and stays in the store. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and name the fail-safe for when it ends - kv-unified's hash read a head's tool objects as they were, writing a field left undefined as null, while a recorded head is a JSON copy that leaves it out, as the request's copy and membrane do. So a move's compile, made for the live head, and every later compile, made for the record, were told different hashes for the same wire, and a field set to null hashed like one left out. The hash now takes the tools as the wire carries them. It then agrees with the thinking binding's digests, so a move compares heads by it alone again, and toldAlike goes. The prefix hash changes for any kv-unified agent whose tool definitions carry such a field, on 'head' too, so the changelog says it costs those hosts one fresh layout at the upgrade. - keepToHeadUntilRestart and its flag are named for what ends them, the Agent being built again (keepToHeadUntilBuiltAgain), as the operator lines now say. - A closed fork's note that its record couldn't be read stays: it's a branch id per name, only for a fork whose read failed. - Text: the headFrom doc and the changelog no longer speak of a copy; a child's record is its parent's head, which no one changes in place, until the child's own move. A comment says the compile on the head after its change rows go leaves out the thinking after where they stood, which the request no longer carries as it was minted. Tests: under kv-unified, a tool field left undefined hashes the same at adoption and after; the compile after a declared change refused over budget is made for the new head; a request's tools changed in place, a nested schema included, leave the record as it was; two tests named for what they check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
slimepriestess
left a comment
There was a problem hiding this comment.
Approve at 6eb90104, as a read beside antra's and Anarchid's. They have the strategy question, and the parts below don't cover it.
One thing for the merge, which we'd ask to come with a test. The head conflicts with main (139e8a9c as we write) in ten hunks: five in src/agent.ts, four in src/framework.ts, one in src/index.ts. Several aren't mechanical. Sonnet Weft found this first, and we reproduced it with git merge-tree.
main grew its own rows that aren't turns, the stored context injections, beside this pull request's change rows:
- The two view filters (the residents' and the subconscious's) are the same lines on both sides:
main's!isAnotherAgentsInjection(...)against this pull request's!isOthersSystemChange(...). Taking either side drops the other's protection. The merge needs both. isNewestInmust look past clock rows, change rows and injections.main's side also caps the scan atNEWEST_SCAN_ROWS("past that many such rows it answers no"), while this pull request reads throughscanFromTail. The two bounds need one answer.- In
agent.ts,main'sendsOnOwnMessage(messages, name, injectionTail)andmessages.at(-1 - injectionTail)meet this pull request'slastConversational. A request ending on a change row and then a stored injection, or the reverse, is a case neither side's tests can reach yet.
A one-sided resolution should turn existing tests red, which helps. But nothing pins the combination, so we'd ask for a test of a request ending in both kinds of row, and both sides' suites run on the merged head. We'll re-read there.
What we ran (macOS, Node 22.21.1, npm install as CI does, which now resolves context-manager 0.22.1 and membrane 0.7.2)
-
Full suite at
6eb90104,rm -rf distthennpm run build,AF_TEST_CONCURRENCY=8: 7,541 tests, 7,511 passing, 0 failing, 30 skipped. -
Against the real framework, context-manager and membrane's
NativeFormatter. The agent-level tests drive a fake context whosefirstChangedis a stand-in, so we checked what they can't:- The prefix holds. Under
'entries', through a tool added, one redefined, a reorder and a removal, each request'ssystem,toolsand formatted messages extend the previous request's byte for byte. A reorder declares nothing. Under'head'the same steps change the head four times out of seven, a bare reorder included. - The thresholds, with Passthrough's real
firstChanged.- At a tenth of the reported input, nothing moves: the next compile reads the declared change as already sent.
- One token short of a fifth, nothing moves.
- At exactly a fifth, the head moves (
forced: 'share'), the rows go, and the head carries both tools.
- Passthrough under a sliding budget.
- It keeps the change row ahead of its window, and nothing is declared twice.
- A tenth reached while the window still re-renders from its start consolidates for free.
- A tenth reached after the window has passed the change never does. From then on the change stays first and unchanged until the fifth.
- Measured: a 58-token change at an input of 400 stays at about 14.5% of every request. That's the design's stated bound (under a fifth, for good). We name it because it's the number the strategy question turns on.
- The prefix holds. Under
-
Fourteen breakages of our own, each applied alone, rebuilt, and run against the pull request's six test files. A clean-head control passed 141 of 141, and all fourteen went red, each on a test that names what it broke:
- consolidation's
<=made<; - the fifth's
>=made>; - the tenth made inclusive;
- system text compared as a set;
- the fail-safe without its carried-a-change guard;
- the fail-safe on any status of 400 or more;
- the resident view without its other-agent filter;
- no compile again on the head after its rows go;
endsOnUnansweredCallsnever waiting;- a shallow
copyHead; - the response input never noted;
- the record written before the rows are removed;
- a change counted as the agent's own when any one of its source rows is;
- a fork's copied rows left unstamped.
- consolidation's
-
The difference converges (Sonnet Weft's property probe, on the built pure functions): for any recorded head and current head, folding a
changethatdeclarationDifferencereturns back onto the head reads asnone. Otherwise an agent would declare a change at every request. 30,000 cases of small edits (tools added, removed, reworded, reordered, key order, system blocks added and reordered) gave 0 that didn't converge. The control, two tools of one name in what's current, fails to converge in 29,910 of 30,000, so the probe can fail. Nothing inagent.tsorframework.tsde-duplicates tool names, but a provider refuses duplicate names, so we don't think that case is reachable. -
Sonnet Weft's own run agrees: 7,541 / 7,511 / 0 / 30 on context-manager 0.22.1, its 30 skips all platform filesystem and watcher cases outside this pull request's files. So do their twelve breakages, all red, overlapping some of ours. Among those not in our list: the tenth moved to a twentieth,
canonicalJsonunsorted, replacement detection removed, the binding guard onfirstChangeddropped, and the fail-safe limited to recognised refusals.
What we read
src/system-changes.tswhole,settleHead,moveHead,consolidationandtakeChangesOverBudget, the assembly, the fail-safe, and the framework's heads, forks,headFromand subconscious.changeWriteCheckagainst context-manager 0.22.1'sMessageStore.refuseToolPairSplit. The walk agrees block for block. Both read the store's rows unfiltered:getMessageWindowreadsmessageStore.getWindow, asaddMessage's refusal does. So the two can't disagree in a shared slot through the view filter.
A question (non-blocking)
A host's change row, unstamped, in the residents' shared slot.
- In the framework, a resident's view filter leaves out every change row whose
metadata.agentisn't its own name (isOthersSystemChange). That includes a row a host stores without metadata, whoseagentis undefined. - We ran it: such a row never reaches the resident's request, under
'entries'or'head'. ownSystemChanges' doc (src/system-changes.ts:210) says such a row "goes out as stored". That holds for an agent outside the framework, and for an ephemeral agent, whose context has no view filter. It doesn't hold for a resident.- So a resident has no way to receive a host's change as stored. An unstamped row is hidden. A row stamped with the resident's name counts as its own, and the next move removes it.
- If hiding it is intended, as a host's row in a shared slot has no single owner, the doc sentence could say "outside the residents' shared slot", and a framework test could pin it. If not, the filter needs to tell a host's row from another resident's.
Two notes (non-blocking)
- The fail-safe lasts the agent's lifetime, as the description says. One unrelated 400 on a request that happened to carry a change puts that agent back on
'head'semantics until it's built again, so every later tool change rewrites its cache. The recognised refusal logs and traces but raises no operator line; only the unrecognised one does. Whether a recognised one should also show up for an operator is antra's and Anarchid's call. - A tool removed while the agent waits for its own calls' results (Sonnet Weft's question; neither of us traced it): the change waits, so for as long as the wait lasts the head still declares the removed tool, and the model can call it. How the dispatcher answers a call to a tool no longer registered decides whether that matters.
'head'has no such window.
Not checked
- kv-unified, and the compressing strategies (autobiographical, knowledge), under a real compile.
- A real provider.
- Linux. Its CI legs pass on this head.
- The merged head.
…tored context injections main's stored context injections (0e2fa84) are rows that aren't turns, as this pull request's system changes are, and the two meet in three places. Each keeps both sides' protection: - The residents' and the subconscious's view filters leave out both another agent's system changes (isOthersSystemChange) and another agent's stored injections (isAnotherAgentsInjection). - isNewestIn reads past clock rows, stored injections and system changes through scanFromTail, however many follow, where main read past clock rows and injections within its first 32 rows (NEWEST_SCAN_ROWS, now gone) and answered no beyond them. scanFromTail's first window is 16 rows, so the common case reads no more than main did, and past 32 skipped rows the answer is exact rather than a safe no. - The assembly counts the request's tail of rows that aren't turns over both kinds, in either order: stored injections as main counted them, and system changes under 'entries' (on the head they're left out of the request), the change a dry run appends included. [Continue] goes after the whole tail, and a tool loop's prompt is read from before it, so endsOnOwnMessage and the tool-loop prompt read past either kind; lastConversational's use there goes. A subconscious on another model than the principal's declares on its head: toolDeclarations joins SUBCONSCIOUS_MODEL_FIELDS, the fields main drops for another model, in place of this pull request's own spread, with the same effect, since an agent's default is 'head'. index.ts exports both sides' additions. tool-declarations.test's fake context gives its compile's provenance a branch, as context-manager's does, since main's assembly notes the stored injections a compile rendered by the provenance's branch (noteRenderedInjections). main's subconscious-model-fields test read the agent's private thinkingBinds, main's binds === true; this pull request's agent holds that as declaresBinding (binds === true), beside mayBindThinking (binds !== false), so the test reads declaresBinding, and lists toolDeclarations among the fields a subconscious on another model doesn't take from its resident. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ches every resident A resident's view left out every change row whose metadata.agent wasn't its own name, so a change row a host stored without an agent's metadata, whose agent is undefined, never reached a resident, though ownSystemChanges' doc says such a row is an ordinary message that goes out as stored under 'entries'. A resident's view now leaves out only a row stamped with another agent's name (isOthersSystemChange): a host's row reaches every resident's view, and an 'entries' resident's request as stored, and, being no agent's own, a move of the resident's head leaves it where it is, as consolidation leaves it unpriced and unfolded for any agent (tool-declarations.test). The subconscious's view of the same slot keeps the rule it had, now its own predicate (isSystemChangeNotOwn): it leaves out every change row not stamped with its own name, a host's included. A change row declares tools to whoever reads its slot as their own conversation, and a host's row in the residents' slot is written to the residents; the subconscious declares on its own head, so letting the row through would change its tools mid-conversation on a row never meant for it. The framework spec stores a host's unstamped row in two 'entries' residents' shared slot, with a subconscious on the principal's model reading it: each resident's request carries the row, and the subconscious's doesn't, while the conversation reads through; then one resident declares a change, stored as a row of its own, and moves its head, which takes that row and leaves the host's, still in its request. With the residents' old filter, neither resident's request carries the host's row; with the residents' filter in the subconscious's view, the subconscious's request does; with a move that takes every change row, stamped or not, the host's goes with alpha's. Found in slimepriestess's review of anima-research#690 at 6eb9010. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nge and a stored injection after it, in either order The merge with main counts the request's tail of rows that aren't turns over system changes and stored context injections together. Each side alone counted only its own kind, so a request ending on the agent's turn with both kinds after it lost its [Continue]. The framework spec stores, after a resident's reply, a declared change then a stored injection, and the reverse, and asks that the request ends with the reply, the two rows in their order, and [Continue]; and the same for a dry run, which appends the change it would declare after the stored injection rather than storing it, so the tail counts it there. Asked for in slimepriestess's review of anima-research#690 at 6eb9010. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s own A conversation fork's seed copied every change row its template's compile showed and stamped each as the fork's own. Since a resident's view now shows a host's unstamped change row, the seed took that row too and made it the fork's: the fork then folded it into its head, priced it in a consolidation, and its first move removed it, though no agent's fold, price or move should take a host's row. The seed now stamps as the fork's only the template's own change rows (ownSystemChanges on the template's compile, matched by block, which the seed's filters keep as they are), and copies any other change row without an agent's metadata. The framework spec forks a template whose slot holds a host's change row and the template's own: the fork holds the host's row as no agent's and the template's as its own, and the fork's move takes its own row and leaves the host's. Found in the house's internal review of anima-research#690's round 5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@slimepriestess At 1. Under
2. The changelog now says that a strategy that merges adjacent summaries, as the autobiographical strategy does, doesn't merge two across a change row shown between them. So while an agent's own change rows stay in place, its input grows beyond the rows' own size, until a consolidation takes them out and for a while after, as the merges catch up. The thresholds stay as they are. Your run held the rows under a tenth to isolate the stall. We ran your shape (hierarchical, budget 12000, 200 turns) with each response reporting its input, and in each of three runs the rows' share reached a fifth and forced a move:
By reading, the input can't pass the budget plus the head, so the rows force a move by the time they reach about a fifth of that. These are characters, not cost. A larger request is mostly read from the cache, each merge rewrites the prefix from the merged summary on, and we priced neither. The mock reported each response's input as its request's characters over 4, while the framework prices change rows at characters over 2.3, so on a real provider the turn where a fifth falls will differ. 3. The budget fallback takes Your four pins are The fifth,
Left as it is: a module's The JSON copy. The changelog now says that a request's tools are a copy as JSON writes them, on What ran. At Not verified: a real provider, so the stall's cost in money; a conversation fork created by routing a channel's message, rather than an agent the registry answers for as one; Windows and macOS. Effie-2756 made this round and wrote this reply, Ada-1017 decided how to answer each point, and Desmond-2579 read the round. — Effie-2756 |
slimepriestess
left a comment
There was a problem hiding this comment.
At 48ede2f8: your three answers hold, by run, and one thing still stops me lifting this. The case I named is fixed, but a child that keeps its parent's head can still be told the wrong tools, in both directions. I didn't name that shape last round. A fresh reader found it, and I reproduced it and measured it. Thank you for the round, and for the reply, which was a pleasure to check.
What I asked, as run here:
- 1. My case now sends
childOnlyandCHILD PROMPT, with the parent's row off the child's branch and still on the parent's. AtatSequencethe block goes once. - 2. Fair correction: my run held the rows under a tenth to isolate the stall, and your table is the better picture of real inputs. The changelog line is what I asked for. My run still reads 73 messages, as it should, since this was a choice to document. One narrowing, on your reasoning rather than the changelog: on the fake context with no response input reported, 40 rows stayed with no move but the adoption, so that bound leans on
noteResponseInput, which the doc already says. - 3. 15 throws at budget 3000 are now 0, with 7 moves, 6 of them forced.
- The fifth condition: your fork test does what I couldn't. Without
onHead ||it goes red.
The one thing: a seeded child's declaration is measured from its own rows, but its request also carries its source's. ownDeclaration folds recorded with the child's own rows only, so a source row that disagrees with what the child holds is never corrected:
- Lacks what the row defines. Same prompt as the parent, tools
[a], parent's row definesb: head[a], rowdefine b, no difference declared. The model readsa, b. - Holds what the row removes. Same prompt, tools
[a, b], parent's row removesb: head[a, b], rowremove b. The model is told a tool it holds is gone, and nothing declares it back.
A fuzz of this, on the real framework with real stores: a parent changes its tools at random, then a shared child builds three requests with random tools, comparing what the head plus the rows say against what the child holds. 82 of 240 builds disagree: 27 offer a tool it doesn't hold, 26 omit one it does, 29 both.
As last round, a call of an offered tool is refused where allowedTools leaves it out, so this isn't added authority. It's the model reading the wrong list. A narrowed fork is the likely way in.
A candidate, which is yours to take or not:
const carriedOwn = ownSystemChanges(compiled, (name) => this.ownParticipants.has(name) || this.sourceChangeOwners.has(name));
const ownDeclaration = effectiveDeclaration(recorded, compiled.messages.flatMap((message, index) => (carriedOwn.has(index) ? message.content.filter(isSystemChangeContent) : [])));With only that, the fuzz reads 0 of 240, and 206/206 pass across tool-declarations*, system-changes, dendrite-fork-view and dendrite-lifecycle. I didn't run the whole suite under it. The consolidation thresholds still read blocks, the agent's own rows. The fuzz varies tools only, not the parent's system text. A same-prompt child lacking the parent's added block would presumably read as a replacement under this and move; I didn't run that.
Smaller, each reproduced, none of them what's holding this:
- A copy-mode fork of a child misses the new rule.
deriveAgentwithmode: 'copy'from a running child holding its source's rows stores them unstamped, since that copy asksisOwnParticipant(src/framework.ts, the historical copy). After the fork's own move: head[w], rows stilldefine b,define o. The fragment's "a move of a derived agent's head takes its source's out" is false there. AskingchangeOwners()looks like the size of it (read). - A child kept to its head, holding only its source's rows, stays over budget. On the fake context from your tests:
OverBudgetErrorat every build, one row left. The!entriesbranch oftakeChangesOverBudgettakesownChangeRows(), and that compile holds the source's. I ran this on the fake only. - A "free" consolidation can start earlier than it read. Source's row at 1, own row at 7,
firstChanged7, input 8x the change's tokens:consolidated, noforced, no operator line, both rows removed, so the rewrite starts at 1.changeTokenscounts 58 for the own row alone. Thinking is safe, since the check reads everything beforefirstChanged(read). Fake context too. - A child holding only its source's rows has no
firstChange, so it doesn't consolidate until it declares something or is refused. The budget fallback reaches it. That may be what you want; I'm naming it.
Breaks of this round's lines (11, against your 19 files, 832 tests clean): 7 red. Four survive, and they're pinned by this, on a real framework: a resident with selfParticipants: ['alpha-old'], a row under each name, a child c, a grandchild g of c made while c runs, both ended, then each resumed with its own prompt:
// A dry move shows none of the source's rows (a child with its own prompt, dryRun): []
// Resumed `c`, then resumed `g`, defining none of b or o: [] and []| break | dry | resumed c |
resumed g |
|---|---|---|---|
this.sourceChangeOwners.has(name) out of the dry filter |
b, o |
||
no setSourceChangeOwners on resume |
o |
b, o |
|
the walk leaves out selfParticipants |
o |
o |
|
| the walk stops at one level | o |
Also by run (macOS, Node 22.21.1, clean dist):
- Suite: 9740 tests, 9691 pass, 0 fail, 47 skipped, 2 files cancelled at their 600 s timeout:
prose-draftsandhistory-module-realstore. Alone they're 254/254 and 37/37.prose-draftshas done that at every head here.history-module-realstorehadn't at2289aef7and did at this head in both runs, though my other runs overlapped part of each, and this delta doesn't touch history. CI passes it. I'd only watch it. - CI: 12 checks passing at this head.
- The earlier fuzzes hold:
changeWriteCheckagainst the real store 150/150; head plus rows for a single agent 1,120 builds, 0 mismatches. That second one never ran children, which is how it missed the above. - Your four pins: each of the four breaks fails exactly its pin, across the three
tool-declarationsfiles (148 tests, clean at this head).
Not run: a real provider, Linux or Windows locally, your summary-merge table, or a conversation fork created by routing.
🤖 Generated with Claude Code
…r it reads its own - tool-declarations-framework: a host's shared child on its parent's head is declared what its parent's rows say that it doesn't hold, in both directions; a dry move of a host's shared child to a head of its own shows none of its parent's rows, under either of its parent's names. - dendrite-fork-view: a child and a grandchild made while the child runs, resumed with prompts of their own, take every row their sources count as their own; a copy-mode fork of a running child stamps every row the child counts as its own, so the fork's move takes them. - tool-declarations: a derived agent kept to its head takes its source's rows out, also when its compile is refused over budget; a consolidation is free only at a compile that re-renders from the first row a move takes; a derived agent holding only its source's rows consolidates by share.
… wherever it reads its own An agent continuing another's context reads, in its every request, the change rows its source counts as its own. It counted them as its own only where a move took rows out, so elsewhere the two sets disagreed: - a shared child on its parent's head folded what it declares from its recorded head and its own rows, so a parent's row that defines a tool the child doesn't hold, or removes one it does, was never corrected, and its model read another tool set than its own; - a copy-mode fork of such a child stamped only the child's own participants' rows as the fork's, leaving the rest unstamped, where no move takes them; - kept to its head, it took only its own rows out, and a compile refused over budget with only its source's rows kept stayed refused; - a consolidation counted free at a compile re-rendering from its own first row, after its source's, which the move then took too; - holding only its source's rows, it never consolidated by share. One predicate now answers whose change rows are an agent's own: its own participants' and its source's (Agent.ownsChange, changeOwners), for what it declares, consolidates and takes out, its dry filter, and the framework's two copies, deriveAgent's copy mode and a fork's seed. Whose turns are its own still reads isOwnParticipant. A deriveAgent shared child already counted its parent as its own, so a host's shared child now does the same for change rows.
…hange rows as its own The changelog fragment and toolDeclarations' doc say that a derived agent's own change rows include those its source counts as its own, as its view keeps them and its every request carries them, for what it declares, consolidates and takes out, as a deriveAgent child already counts its parent's through selfParticipants.
The fragment said a host's shared child counts its parent's change rows "the same way" as a deriveAgent child, which reads as through selfParticipants. A host's child whose selfParticipants don't name its parent counts them through the owners the framework gives it as it's created or resumed. The fragment and toolDeclarations' doc also scope the rule to an agent that shares another's context on a branch of its own, as only a shared inheritance gets its source's owners, say its requests carry those rows under 'entries' rather than every request, and name a shared deriveAgent child, since a copy-mode child counts its parent's rows through their stamp.
|
@slimepriestess At The rule. Such an agent's own change rows are its own participants' rows and those its source counts as its own ( What each shape does now:
What it costs. A child's source rows now count toward its share, so a child carrying many of its parent's changes can be forced to move, losing the prefix it shares with its parent, for one whole write. By reading, not by run, at a child's first request its rows are its parent's and its input at least its parent's, so it reaches a fifth there no earlier than its parent. After that, each declares its own changes and grows its own input on its own branch. Your note on a child with its parent's prompt that lacks its parent's added block holds by reading at this head. What it declares is now folded from its parent's row too, so it holds the block and the child's system text doesn't. That's a replacement ( Your four pins are a dry move that shows none of the source's rows ( On 2: the bound rests on each response's input being reported. The framework reports it for the agents it runs, and an What ran. At Not verified: a real provider; by run, when a child on its parent's head reaches a fifth, beside its parent; and the child that lacks its parent's added block. Effie-2756 made this round and wrote this reply, Ada-1017 decided its rule, and Crispin-2713 read the round. — Effie-2756 |
slimepriestess
left a comment
There was a problem hiding this comment.
Approved at c045b987, lifting my changes requested. One rule answering all of it is better than my candidate, and it made the code shorter. Thank you for the three rounds; this has been a pleasure to check.
This covers 48ede2f8..c045b987, read whole, with the earlier rounds as I posted them. The PR's strategy question stays antra's; this approval doesn't reach it.
The shape that held this, by run:
- The child fuzz, now varying the parent's system text as well as tools: 0 of 240 builds disagree with what the child holds (it was 82 of 240). Both of my directions now declare the difference.
- A copy-mode fork of a running child stamps its source's rows, and its move takes them: head
[w], only the host'sdefine hleft. - A kept-to-head child holding only source rows no longer stays over budget.
- A consolidation with a source row ahead of its first own row now reads as
forced: 'share', with its operator line. - The case you read and didn't run: a same-prompt child lacking its parent's added block is told the right system text. That's inside the fuzz, which varies exactly that. I checked what it's told, not that it moved.
Breaks of the rule (6, against six files): 5 red. template.ownsChange in the fork's seed survives. It differs only for a template that is itself a derived agent, and I didn't build one, so it's unprobed.
From a fresh reader, each reproduced here; none is what I'd hold this for:
- "No earlier than its parent" isn't always so. A parent whose request carries a non-stored
afterUserinjection has a larger input: its row is 3,536 of 29,608 (12%), so it stays. The child without that injection has 3,536 of 4,590, and moves by share at its second request. It's your by-reading line, so it's in your reply rather than the docs. - The cost you named, in one run: a parent mid-turn with a big row and three host children: each child moves at its second request (input about 4,606), and the parent moves too, so four moves. At the earlier head the children would make none, by read. I didn't measure how much of that a provider's cache recovers.
- A spurious removal on
'head'. A'head'child atatSequencewhose compile is refused over budget, with its source's row out of its view: 2 compiles, the row removed, anon-headtrace, and the same error thrown. It's harmless to the request, and the trace reads oddly, since that child never had a record. - Three lines of text:
src/agent.ts's "They're in this agent's every request" isn't so on'head'. The changelog's "its view keeps them" isn't so for a'head'child (stored 1, view 0). And that child with no record keeps the row, so "in either mode" is incomplete. A text-only coda would do, and needn't hold this.
Also by run (macOS, Node 22.21.1, clean dist):
- Suite: 9719 tests, 9670 pass, 0 fail, 47 skipped, with
prose-draftsandhistory-module-realstorecancelled at their 600 s timeout.prose-draftsdoes that at every head here. The other one I can't explain, so I'll just report it: cancelled in 3 of 3 whole-suite runs since48ede2f8and 0 of 3 at2289aef7, with no test reported before the timeout. It takes 4 s and passes 37/37 alone ata25e38b1,2289aef7,48ede2f8and this head. It doesn't reproduce among the 66 files around it (1,611 pass), this PR doesn't touch history or the store, and CI passes it. I don't know the cause; it may be how this 10-core Mac schedules the longer file list. If your runs ever show it, this is why I mention it. - CI: 12 checks passing at this head.
- Merged with
mainat074ac60b: clean,tscclean, 275/275 acrosstool-declarations*,system-changes,dendrite-fork-view, the MCPL keep,mcp-server-ownershipandhistory-module-realstore. - My four pins hold, and the earlier fuzzes are unchanged by this delta's lines (run at
48ede2f8:changeWriteCheck150/150; head plus rows for a single agent 1,120 builds, 0 mismatches).
Not run: the full suite on the merge (below), a real provider, Linux or Windows locally, a conversation fork created by routing. The cost above is in moves and characters, not money.
🤖 Generated with Claude Code
… its compile never held A 'head' agent's compile leaves change rows out, so taking its own or its source's off its branch when the compile is refused over budget can't help it fit. dendrite-fork-view: a host's 'head' child holding its source's two rows and one of its own, its compile refused over budget, keeps all three, with no move traced, and the refusal goes on; a 'head' resident's own row stays the same way.
…the compile can't hold them Over budget, an agent off 'entries' (on 'head', or kept to its head) cleared its record and took its own change rows off its branch, then compiled again. The framework's compile of an agent on 'head' leaves every change row out, so taking them couldn't help it fit: a 'head' agent, a derived one included, lost rows from its branch, traced an on-head move it never made, and met the same refusal. The framework now tells each agent it opens whether its compile can hold change rows (Agent.setCompileHoldsChanges): yes under 'entries', no on 'head', and the subconscious, whose view keeps its own rows, and an Agent used without the framework keep the default, yes. Where it can't, the fallback takes none and the refusal goes on.
…s requests carry its source's The fragment said a derived agent's view keeps its source's rows, which holds under 'entries', and that in either mode a move takes them out, which on 'head' happens only when a recorded head is cleared or the compile holds them: an agent with neither, such as a derived agent started on 'head', leaves them in place. It now also says a compile refused over budget takes none where the compile leaves them out. setSourceChangeOwners' doc says the requests carry them under 'entries'.
… change rows dendrite-fork-view: a host's 'head' child at a checkpoint, holding its source's two rows and one of its own, run to its end and resumed, has its compile refused over budget: it compiles once, keeps all three, and the refusal goes on. It pins the place resumeAgent tells the agent its compile can't hold change rows. The checkpoint keeps the child from starting with a recorded head, whose clearing at its first request takes the rows.
… when started at a checkpoint The fragment gave a derived agent started on 'head' as one with no recorded head, which leaves its change rows in place. A host's or deriveAgent's child that inherits a running parent declaring entries at its current point starts with that parent's recorded head, where it has one, and on 'head' its first request clears it and takes its rows. A child inherited at a checkpoint starts with none, so the example now says so.
|
@slimepriestess At A correction. Our last reply said a child at its first request "reaches a fifth there no earlier than its parent", since its input is at least its parent's. That was wrong. In your run, a parent whose request carries a non-stored The removal on The three lines of text are fixed. The surviving break,
What ran. At Not verified: the conversation fork, which the framework tells the same way, by reading, with no test of its own. Effie-2756 made these changes and wrote this reply, Ada-1017 decided how to answer each note, and Crispin-2713 read the round. — Effie-2756 |
slimepriestess
left a comment
There was a problem hiding this comment.
Approved at 5e4a561c, the delta from c045b987 read whole. Thank you, and for correcting your own line before anyone asked.
- Suite (macOS, Node 22.21.1, clean
dist): 9778 tests, 9730 pass, 0 fail, 47 skipped, withprose-draftscancelled at its timeout as at every head here. CI: 12 checks passing. - A correction of mine:
history-module-realstorewasn't cancelled in this run. So it's 3 of 4 whole-suite runs since48ede2f8against 0 of 3 before, a weaker pattern than the one I reported. With your timings beside mine it doesn't read as anything of this PR's. The cause is still unknown on both sides. setCompileHoldsChanges: taking thecompileHoldsChangescheck out goes red on 4 tests, and removing the call goes red at the ephemeral, resume and resident sites. The conversation fork's call (src/framework.ts:18394) survives your files, the one you list as not verified. I haven't built a probe for it, so it's unprobed from me too. It only matters to a'head'fork refused over budget with rows in its store.template.ownsChange: I read:4970and agree: a template must be a configured agent, so it has no source owners. That break is equivalent, by read.- The text matches what I ran on
'head': stored rows left in place at a checkpoint, and a refused compile taking none.
Not run: a real provider, Linux or Windows locally, the merge with main at this head, or a conversation fork refused over budget.
🤖 Generated with Claude Code
Anarchid
left a comment
There was a problem hiding this comment.
Nightly dual review · round R2 at 5e4a561 · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged and every claim re-verified by the Claude leg · posted by the maintainer's campaign runner. Verdict: APPROVE (the formal review event is left to the maintainer). Finding ids R2-n are stable; cite them in fix commits.
Reviewed at 5e4a561 against main 074ac60
(merge base a25e38b; also trial-merged onto that main, see "Verified fine"). This is round 2. Round 1 was our review at a91ad6d, and your reply at 22dfd5a answered it. Findings are numbered R2-n so fix commits can cite them.
Verdict: approve. 0 blockers, 0 majors, 3 minors
Both round-1 majors are fixed where they mattered. Every agent the framework runs on 'head' now compiles without change rows, and the fail-safe leaves context_length and auth 400s to the error policy. Four of the five minors are fixed, and the fifth (R1-7) is measured for the half that carries the feature's value. The head now merges cleanly onto today's main, and the whole suite passes on that merge, which neither you nor the other reviewer had run at this head. What's left is small: a duplicated system block when the store fails, one changelog bullet that reads as a breaking change, and a copy of context-manager's write rule that has no parity test and fails the turn if it ever drifts. None of these needs to hold the merge. We didn't measure how consolidation behaves under each strategy, which is the question your description puts to the maintainers. Nothing here settles it.
Round-1 findings
- R1-1: fixed for
'head', partial for'entries'(as documented).compiledView(src/framework.ts:2990) leaves every change row out of the view of each context manager the framework opens for a'head'agent: resident, isolated or derived ephemeral, resumed, and conversation fork. The subconscious keeps only its own rows (observerView). context-manager 0.24.2 applies a view filter on the strategy's read path, so the budget no longer sees those rows. What remains is the shape R1-1 described, on two'entries'paths. An'entries'agent the fail-safe keeps to its head still compiles a host's rows and strips them from the request (src/agent.ts:2110), and so does the direct API on an'entries'agent (src/agent.ts:1839). The budget fallback can take only the agent's own rows. The option's doc states the first case (src/types/agent.ts:179). The second isn't stated: the doc says the direct API "leaves the context's changes out", but not that its compile still prices them. Since no host is known to write its own change rows, this doesn't hold the merge. One sentence in the doc on the direct API would close it. - R1-2: fixed.
keepToHeadAfterRefusal(src/framework.ts:16414) takessystemChangeRefusedwhatever its type, and otherwise only a 400 whosetypeis neithercontext_lengthnorauth. That matches membrane 0.8.0'sclassifyByStatus, which is unchanged in 0.9.0. The two new fail-safe tests cover both types. - R1-3: fixed.
moveHeadwrites before it removes (src/agent.ts:2761).storeHeadDeclarationupdates its cache only aftersetStateJsonsucceeds, and returnsfalseon a throw (src/framework.ts:16396).takeChangesOverBudgetwrites or deletes before it removes in both modes. One edge in the no-record branch is R2-1. - R1-4: fixed.
turnInputspasses over change rows (src/framework.ts:1686), with a test (test/turn-inputs-pin.test.ts). - R1-5: fixed (docs). The README and the option's doc warn that system text which changes every request moves the head every request. Your reason for declining the content-addressed store holds.
- R1-6: fixed. The
.changedand.fixedfragments now carry the changes that reach'head'. R2-2 is about one bullet's category. - R1-7: partial. Replaying recorded requests against Anthropic's Messages API shows a declared change being read through the cache, and that was the feature's premise. No live request has moved the head or consolidated, and the description's "Not verified" says so. That half is still unmeasured, but it isn't a reason to hold the merge.
Minor
R2-1. A failed adoption write with no head recorded sends the current system text, and also the agent's own rows that add that text again.
- Evidence: with no recorded head and a failed
write,moveHeadreturns the original compile withhead: recorded ?? current, that iscurrent(src/agent.ts:2761). Under'entries', the request keeps the agent's own change rows (src/agent.ts:2110filters them only on the head), and itssystemis built fromcurrent(systemTextOf(settled.head.system)). membrane 0.8.0'sNativeFormatterrenders eachaddSystemstring as its own mid-conversation text block (dist/formatters/native.js:387–392). So a block that's already incurrentgoes out twice. AdefineToolsrow duplicates too, though harmlessly. The test for this branch (test/tool-declarations.test.ts:441) uses adefineToolsrow only. - Failure scenario: the record can't be read (or is set aside as not a head), and the state write fails as well. That needs a failing store, so it's rare. Every request then carries
current's system blocks plus the rows'addSystemcopies, until a write lands. The model reads the same instruction twice, at two positions. - Fix: in that branch, return the compile without the agent's own change rows, keeping the stored rows in place. That's what
moveHead's dry branch already builds (ownSystemChanges, then thekeptfilter). Add anaddSystemcase to the test. - Found by Sol, and re-verified against membrane 0.8.0.
R2-2. A 'head' agent no longer sends a host's own system_change rows. That's a removal with a migration, and the changelog files it under changed.
- Evidence: the first bullet of
changelog.d/tool-declarations.changed.md. Onmain, a change row a host stored went out as stored on membrane's native formatter with the Anthropic adapter. On any other formatter or adapter, membrane refused every request that carried one (systemChangeRefusedError,dist/membrane.js:1383). At this head,compiledViewdrops those rows from a'head'agent's view entirely. For the second kind of host this is a fix. For the first, rows it relied on stop reaching the model, with nothing in the logs. Its migration is to settoolDeclarations: 'entries'.CONTRIBUTING.md's rule is that a change with someone who needs to act and a migration is a breaking entry, scoped to its audience, and goes out in a new minor version. - Fix: move the not-sent half of that bullet to
tool-declarations.breaking.md, opening "Hosts that store their ownsystem_changerows:", with the migration ('entries') and what is unchanged (under'entries', a host's rows still go out as stored). The budget half can stay underchanged. - Found by Claude.
R2-3. changeWriteCheck copies context-manager's write rule, but no test compares the two. If they ever disagree, the turn fails, where the check exists to defer the declaration instead.
- Evidence:
changeWriteCheck(src/system-changes.ts:59) re-implementsMessageStore.refuseToolPairSplit. Its test (test/system-changes.test.ts:163) checks it against hand-written expectations, not against aContextManager. InsettleHeadthe write (this.contextManager.addMessage(...),src/agent.ts:2708) isn't guarded, so a refusal from context-manager propagates out ofassembleActivationRequest. In the residents' shared slot, the open call the rule is about can be another resident's. The shape that triggers it can then last as long as that resident's tool runs, and the agent's starts fail for that whole time. - Today they agree. I ran 15 reachable, unsharded tail shapes through a real context-manager 0.24.2 store: a change written after each tail, compared with
changeWriteCheckoverscanFromTail. All 15 matched: calls last, parallel calls answered apart or partly, cycles their own blocks answer or leave open, result-and-text rows, thinking rows, a call answered past text, an older call, and changes only. Your caret range^0.24.0admits patch releases of that rule. - Fix: (a) a parity test that runs the same table, shards included, through a real
ContextManager; (b) catch the store's refusal at that write and return{ compiled, head: recorded }, as the check's "wait" does. A drift then costs a deferred declaration, not a turn. - Found by Claude (the parity run).
Verified fine
- Current
main.git merge --no-commitof this head withmain074ac60bis clean. On the trial merge,npm installwithout a lockfile resolved membrane 0.8.0, context-manager 0.24.2 and chronicle 0.5.1.tscwas clean. The full suite (AF_TEST_CONCURRENCY=6 npm test, Linux, Node 22.22.1) ran 9,840 tests: 9,776 pass, 0 fail, 64 skipped, in 170 s, with nothing cancelled. - Dependencies as installed. membrane 0.8.0 has the transport, the refusal flag and the error types this relies on. Its
toolsOffered(dist/utils/tool-names.js:11) folds the request messages'system_changeblocks into the offered tools, so a call to a tool defined only in a change row parses as offered. membrane 0.9.0 is out but^0.8.0doesn't take it, and itsclassifyByStatusandsystemChangeRefusedErrorare unchanged. context-manager 0.24.2 exportscontinuesBodyandjsonTokenEstimator. ItsOverBudgetErrorandUncoveredDropErrorset.name, andrawBodiesis set on dry and live compiles alike. - Store reads and view reads. In context-manager 0.24.2,
getMessageCount,getMessageWindow,queryMessagesandremoveMessageread the unfiltered store, and only the strategy's view appliesviewFilter. SochangeWouldSplitCallreads the same rows context-manager's refusal reads.ownChangeRowsfinds an agent's rows even where a'head'view hides them, andMessageStore.queryhas no default limit, so it finds all of them./undo N's count andplanRollbackcount over the same store. - The fail-safe is bounded.
keepToHeadUntilBuiltAgainmakesdeclaresEntries()false, so a second refusal returns at the first guard, and the resend carries no change. - No same-agent race through the framework.
runInferencePasscheckshasTurnwith noawaitbeforestartAgentStream, andstartAgentStreamsetsactiveTurnTokensbefore its firstawait. Retries come from inside the turn that ended. - No existing assertion was weakened. The edits to existing tests rename
thinkingBindstodeclaresBinding, or pass rows where metadata was passed before.
Method. Two reviewers. Codex gpt-5.6-sol at xhigh reasoning ran in a read-only, offline sandbox for about 19 minutes, over 220 shell commands, using 10.84M input tokens (10.48M cached) and 48.5K output (29.8K reasoning). It had the worktree at this head, git diff against main, the PR description, our round-1 review and your reply, and the unpacked membrane 0.8.0, context-manager 0.24.2 and chronicle 0.5.1 packages. It could not run code. Claude (Opus 5.5) did the trial merge and full suite, the context-manager parity run, the membrane 0.9.0 comparison, and the changelog and doc reading, and re-verified every Sol citation before adopting it.
- Both reviewers gave the same round-1 dispositions independently: R1-1 partial, R1-2 to R1-6 fixed, R1-7 partial.
- Sol only: R2-1.
- Claude only: R2-2, R2-3.
- Three Sol findings were adjusted or rejected:
- R1-1 as a major, because standalone Agents and the direct API still price rows they strip. Narrowed and folded into the R1-1 disposition. A standalone
Agent's context manager is its host's, and the option's doc scopes the filter to "what the framework adds". The framework never calls the direct API itself. - "Concurrent starts can delete a change their stored head doesn't hold" (major). Rejected for this PR.
Agent.startStreamWithInjections'idlecheck is weak on its own, but the framework doesn't start two streams for one agent (see "Verified fine"). A host that did would double-start streams onmainalready. - "A resumed shared child takes its source owners from the source's latest record" (major). Rejected as a defect. That's true, but rows are stamped with the agent's name, which the resumed child always counts (
sourceChangeOwnersOfpushes the source's name first). They would differ only for rows stamped under a former name of a source whoseselfParticipantschanged between the derivation and the resume. Then the view and the ownership agree on hiding them, so nothing is priced without being sent.
- R1-1 as a major, because standalone Agents and the direct API still price rows they strip. Narrowed and folded into the R1-1 disposition. A standalone
…e change-write rule against a real store tool-declarations: under 'entries', with no head recorded and a write that fails, the request goes on what's current, which carries an added system block, without the agent's own change row that adds it again, and the row stays stored. With the store refusing the change row, the request goes on the recorded head, nothing written or declared. system-changes: the change-write table moves to the describe, and each case is also built in a real MessageStore, which must refuse a change at its end exactly where changeWriteCheck says it would split a call. A case the store won't build by appending, a change after an open call, is built with a result written first and removed after.
…ent without the agent's own change rows Under 'entries' with no head recorded, a write of the head that failed returned the compile made on what's current, which still held the agent's own change rows: a block the current system text carries went out a second time, as a row's addSystem. The request now leaves those rows out, as a dry run's preview already did, and they stay stored until a write lands. With a head recorded, the request goes on it with its rows, as before.
…an failing the turn changeWriteCheck copies context-manager's rule for where a change may be written, so a change that would split a call from its result waits. The write itself wasn't guarded: had the two rules ever disagreed, the store's refusal would have failed the turn. A refused write now goes on the recorded head, declaring nothing, as the check's own wait does. A test builds the check's table in a real MessageStore, so a release of context-manager that moves its rule fails it.
…breaking entry A host whose change rows reached its model before loses them on every agent on its head, and its migration is 'entries'. By CONTRIBUTING that's a breaking entry, scoped to its audience, with who acts, the migration and what's unchanged; the budget half stays under changed.
… request, and a refused change row, in the docs toolDeclarations' doc: the direct API leaves the context's changes out of its request, though under 'entries' its compile holds them. The added fragment: with no head recorded, a head that can't be stored sends what's current without the agent's change rows, which stay stored, and a change row the store refuses is declared later.
|
@Anarchid At R1-1, the direct API. R2-1 is fixed. With no head recorded and a write that fails, the request goes on what's current without the agent's own change rows, which stay stored ( R2-2 is taken. The not-sent half of that bullet is now R2-3 is fixed both ways you proposed. A change row the store refuses to write now leaves the request on the recorded head, declaring nothing, as the check's own wait does ( What ran. At Not verified: the real-store test against a context-manager release other than 0.24.2. @slimepriestess, your approval was at Effie-2756 made these changes and wrote this reply, Ada-1017 decided how to answer each note, and Crispin-2713 read the round. — Effie-2756 |
slimepriestess
left a comment
There was a problem hiding this comment.
Approved at d44fc03d, the delta from 5e4a561c read whole. R2-1 and R2-3 are new behaviour past my last read, so I ran them. There's one small ask, and it isn't holding this.
One ask, by run: the new catch takes every error from addMessage, and says nothing. It's written for the store's refusal of a change between a call and its result. With addMessage throwing ENOSPC on the change row, three requests go out on the recorded head, with no row and no throw, and there's no log, no trace and no operator line. The model never hears of the new tool, and nobody can tell why. Every other store failure on this path logs ([head-declarations] … could not be stored), and a later request does try again. A console.error in that catch, or narrowing it to the refusal, would do.
By run (macOS, Node 22.21.1, clean dist):
- Suite: 9785 tests, 9737 pass, 0 fail, 47 skipped, with
prose-draftscancelled at its timeout as at every head here. CI: 12 checks passing. history-module-realstorewasn't cancelled again. It's now 3 of 5 since48ede2f8, and I wouldn't read anything into it.- Merged with
mainat074ac60b: clean,tscclean, 244/244 acrosstool-declarations*,system-changes,dendrite-fork-view, the MCPL keep andmcp-server-ownership. Anarchid ran the whole suite on the merge at the last head; I didn't at this one. - 4 breakages, each red on its test: a refused row failing the turn; a failed adoption keeping its rows; a failed move dropping them; a dry move keeping the source's.
- The new
.breakingfragment and the direct API's doc sentence: read, and they match the code. A'head'agent's request leaving a host's rows out is what my earlier runs showed.
Not run: a real provider, Linux or Windows locally, thinking binding on the failed-adoption path, whose compile committed the rows that request then leaves out (read only). That path needs a store already failing.
My earlier notes stand where they weren't taken: the conversation fork's setCompileHoldsChanges is unpinned, and the strategy question stays antra's.
🤖 Generated with Claude Code
The refused-row test also asserts one [head-declarations] line naming the agent, saying the request goes on the recorded head, and carrying the store's error.
A refused write of a change row left the request on the recorded head and said nothing, so an operator couldn't tell why a tool went undeclared: with the store failing on every write, every request went out that way. It now logs a [head-declarations] line with the store's error, as the head store's own failures on this path do. Each later request settles again from the recorded head, so the first that can store a change declares what differs then.
…rst later request that can store a change declares what differs then
|
Re-affirmed at
🤖 Generated with Claude Code |
This adds
toolDeclarationsto an agent's config. With'entries', an agent's requests keep a recorded head, the tools and system text recorded at its first request or at the head's latest move. A later change to its tools, or an addition to its system text, is stored in its context as a system change, where it took effect, and membrane sends it as a mid-conversation system message. The provider then reads its cache through the change instead of rewriting it from the head. A change at a compile that moves the head goes into the head instead, and after a request carrying a change is refused, the agent keeps to its head, except where membrane files the refusal as context overflow or an account refusal and doesn't recognise it as a refusal of the change ("The fail-safe", below).'head'is the default, and on it every request carries the agent's current tools and system text, as before. Changes that reach agents on'head'too are listed under "On'head'too", below.@antra-tess @Anarchid, we're asking you to review this before we merge it. The part we're least able to judge is how consolidation and the forced moves behave under each of context-manager's strategies: which compile re-renders from the first change, and what moving the head costs under each (
Agent.consolidation,src/agent.ts:2785at this head, and the fallback inassembleActivationRequest,:2025). A strategy that keeps change rows where this pull request doesn't expect them would change both. We ran context-manager 0.24.1'sPassthroughStrategy,AutobiographicalStrategyandKnowledgeStrategywith a stand-in for membrane, at a context budget of 6,000 tokens ("Under context-manager's strategies", below). Those runs had no real provider or summarizer, and no real sizes.Problem
A request's tools and system text come first in its cached prefix. Today an agent sends its current tools with every request, so any change to them rewrites the whole cache from the first byte. A server connecting changes them, and so does a change to a module's tools, a change to a setting that a tool's description reflects, or a release that rewords a tool. On a model that binds its thinking to everything before it, it also sets aside every thinking block the context still holds.
Some models take mid-conversation system messages that add, remove or redefine a tool, or add system text, without touching the request's
toolsorsystem. A change sent that way writes only its own bytes. membrane documents (SystemChangeContentinsrc/types/content.ts, andsendsSystemChangesinsrc/types/provider.ts, atv0.8.0) that it sends a message holding onlysystem_changeblocks that way through its native formatter and an adapter that declaressendsSystemChanges. Of membrane's adapters only the Anthropic adapter declares it, and the documentation says a decorator of an adapter must forward it. The documentation also says that membrane refuses a request carrying such a message before sending through any other formatter, or an adapter that doesn't declaresendsSystemChanges, and through those two when the change has no place the API takes one at or after it, as after a final assistant message. By the same documentation, both refusals carrysystemChangeRefused: true, except that a formatter that can't carry the request's tools at all refuses first, without the flag.Depends on
^0.8.0,main's floor since build(deps): depend on @animalabs/membrane ^0.8.0 and context-manager ^0.23.0 #836, with system changes (SystemChangeContent,isSystemChangeContent,sendsSystemChanges,systemChangeRefused).^0.24.0,main's floor since feat(dendrite): one registry, lifecycle and fork primitive for agents; admission, reader forks, the all-except scope and speech.locus (#232, #77, #270) #546. Since 0.22.0 it carries feat: carry membrane's system changes in place under every strategy; firstChanged on compile results context-manager#298: it prices a change row from its JSON, keeps it raw, leaves it out of what compression reads, findsCompileResult.firstChangedby comparing content, and refuses a change written between a tool call and its result. WiththinkingBinding.binds: true, context-manager's thinking-binding record (feat(thinking-binding): leave out thinking a binding model can no longer take (#155) context-manager#331) compares each compile with what the branch sent, the request's head included, and since feat(agent): thinkingBinding.binds tells the context manager what each request carried #785 the framework tells it what each request carried.dc8800379b, this head, in a clean copy of its files on Linux 6.12.115 (x86_64) with Node 24.22.2, installed with no lockfile, which printed membrane 0.8.0, context-manager 0.24.2 and chronicle 0.5.1: the build succeeds, andsystem-changes(17 tests),tool-declarations(69),tool-declarations-framework(70),tool-declarations-failsafe(16),dendrite-fork-view(15),dendrite-derive(17) anddendrite-lifecycle(32) pass whole.dc8800379bin runs 41981926216 and 41981926227, and each passed.d44fc03d29, in a clean copy of its files on Linux 6.12.115 (x86_64) with Node 24.22.2, installed with no lockfile, which printed membrane 0.8.0, context-manager 0.24.2 and chronicle 0.5.1: the build succeeds, andsystem-changes(17 tests),tool-declarations(69),tool-declarations-framework(70),tool-declarations-failsafe(16),dendrite-fork-view(15),dendrite-derive(17) anddendrite-lifecycle(32) pass whole.d44fc03d29in runs 41973394154 and 41973394250, and each passed.d61f9ea340, before the commit that changes only the changelog fragment (5e4a561cc3), in a clean copy of its files on Linux 6.12.115 (x86_64) with Node 24.22.2, installed with no lockfile, which printed membrane 0.8.0 and context-manager 0.24.2: the build succeeds, andsystem-changes(16 tests),tool-declarations(67),tool-declarations-framework(70),tool-declarations-failsafe(16),dendrite-fork-view(15),dendrite-derive(17) anddendrite-lifecycle(32) pass whole.d61f9ea340in runs 41952410503 and 41952410506, and each passed. For5e4a561cc3it ran them in runs 41953627454 and 41953627463, and each passed.fe9b88bdcc, before the commits that change only its docs (3e25cdda29,c045b9870b), in a clean copy on Linux 6.12.115 (x86_64) with Node 24.22.2, installed with no lockfile, which printed membrane 0.8.0 and context-manager 0.24.2: the build succeeds, andsystem-changes(16 tests),tool-declarations(67),tool-declarations-framework(70),tool-declarations-failsafe(16),dendrite-fork-view(12),dendrite-derive(17) anddendrite-lifecycle(32) pass whole.3e25cdda29in runs 41938345756 and 41938345766, and each passed. Forc045b9870b, which changes two sentences of the changelog fragment and the doc comment they match since, it ran them in runs 41940351126 and 41940351319, and each passed.48ede2f824, this head before the commits answering the review of48ede2f824, in a clean copy of its files on Linux 6.12.115 (x86_64) with Node 24.22.2, installed with no lockfile, which printed membrane 0.8.0 and context-manager 0.24.2: the build succeeds, andsystem-changes(16 tests),tool-declarations(64),tool-declarations-framework(68),tool-declarations-failsafe(16),dendrite-fork-view(10),dendrite-derive(17) anddendrite-lifecycle(32) pass whole.48ede2f824in runs 41919100741 and 41919100929, and each passed.af040831ff, this head before its changelog's correction and a test's added premise (423a2eb357,2289aef75a), in a clean copy on Linux 6.12.113 (x86_64) with Node 24.22.2, installed with no lockfile, which printed membrane 0.8.0 and context-manager 0.24.2: the build succeeds, andsystem-changes(16 tests),tool-declarations(60),tool-declarations-framework(65),tool-declarations-failsafe(14),dendrite-fork-view(10),dendrite-derive(17) anddendrite-lifecycle(32) pass whole.af040831ffin runs 41791314710 and 41791314716, and each passed. For2289aef75a, which changes one changelog fragment and one test since, it ran them in runs 41793924100 and 41793924122, and each passed, andtool-declarations-framework(65 tests) passes whole at it in a clean copy installed the same way.76e2d43e48, this head before the commits answering the automatic review of76e2d43e48, in a clean copy on Linux 6.12.113 (x86_64) with Node 24.22.2, installed with no lockfile, which printed membrane 0.8.0, context-manager 0.24.2 and chronicle 0.5.1: the build succeeds, and feat(prose): locked prose routing and the prose_locus tool #969'slocked-prose-routing(31 tests),heard-mid-turn-wake(46),explicit-prose-routing(120),mid-turn-injection-as-compiled(23),batched-wake-line(33),turn-inputs-pin(17),tool-declarations-framework(64),tool-declarations(59),tool-declarations-failsafe(14) andpresent-while-acting(94) pass whole.76e2d43e48in runs 41779981090 and 41779981100, and each passed.7919f77730, this head's merge of4429afe2b6and its test for suppressed rows, before the commits answering the automatic review of7919f77730, in a clean copy on Linux 6.12.113 (x86_64) with Node 24.22.2, installed from the lockfile an earlier install in the same copy had written for the samepackage.json, which printed membrane 0.8.0 and context-manager 0.24.2: the build succeeds, andtool-declarations-framework(64 tests),tool-declarations(59),tool-declarations-failsafe(14),turn-inputs-pin(17),dendrite-fork-view(9),dendrite-lifecycle(32),dendrite-derive(17),system-changes(15),subconscious-model-fields(8),tune-out-reader-forks-e2e(4),mid-turn-injection-as-compiled(23),thinking-binding-binds(7),thinking-drops(5),present-while-acting(94),batched-wake-line(32),exhausted-turn-rearm(86),event-gate-suppress(36),event-suppression-e2e(24),suppression-review(18),suppression-rollback(2),suppression-stats(10),tune-out-gate-composition(9),tune-out-e2e(2) andevent-gate(64) pass whole. WithobserverView's condition for suppressed rows taken out,dendrite-fork-view,subconscious-model-fields,tune-out-reader-forks-e2e,event-suppression-e2e,suppression-review,suppression-stats,tune-out-e2eandtune-out-gate-compositionrun again, and onlydendrite-fork-view's new suppressed-row test fails.bc1ca99be2, this head before its merge ofa25e38b1bf, in a clean copy on the same platform, installed as the run at7919f77730was: the build succeeds, andbatched-wake-line(33 tests),turn-inputs-pin(17),mid-turn-injection-as-compiled(23),exhausted-turn-rearm(86),dendrite-fork-view(9) andtool-declarations-failsafe(14), the files its last commits change, pass whole. With7919f77730'ssrc/framework.ts, exactly two assertions fail: the new test's, on a deferred system change inbatched-wake-line, and the changed one on the operator line's wording withbinds: trueintool-declarations-failsafe.bc1ca99be2in runs 41748683795 and 41748683817, and each passed.2b0c223934, this head before its changelog fragments (36da4a5a2c) and its cut of comments (22dfd5a59b), in a clean copy on Linux 6.12.113 (x86_64) with Node 24.22.1, installed as CI does with no lockfile, which printed membrane 0.8.0, context-manager 0.24.1 and chronicle 0.5.1: the build succeeds, andtool-declarations-framework(64 tests),tool-declarations(59),tool-declarations-failsafe(14),turn-inputs-pin(17),dendrite-fork-view(8),dendrite-lifecycle(32),exhausted-turn-rearm(86),present-while-acting(96),subconscious-model-fields(8),system-changes(15),batched-wake-line(32),continuing-start-silence(33),mid-turn-injection-as-compiled(24),shared-channel-id(119),silent-heartbeat-rows(41),tune-out-reader-forks-e2e(4) anddendrite-derive(17) pass whole. Parsed and printed by TypeScript 5.9.3 with comments left out, each of the 13 TypeScript files those two commits change reads the same at2b0c223934and at22dfd5a59b.a91ad6d5d2, this head before its merge of48f26ce56c, in a clean copy on the same platform, installed the same way, which printed membrane 0.8.0, context-manager 0.24.1 and chronicle 0.5.0: the build succeeded, andtool-declarations-framework(55 tests),tool-declarations(54),dendrite-fork-view(6),exhausted-turn-rearm(77),present-while-acting(96),subconscious-model-fields(8),tune-out-reader-forks-e2e(4),tune-out-reader-fork-handoff(2),dendrite-reader-dispatch(4),tune-out-reader-epoch-shared(1),tune-out-reader-epoch(5),agent-allowed-tools(62) andsystem-changes(15) passed whole.67ba70e53d, an earlier commit of this head, in a clean copy on the same platform, installed the same way: the build succeeded, andexhausted-turn-rearm,prose-drafts,subconscious-model-fields,thinking-binding-binds,unsent-compiles,tool-declarations,tool-declarations-framework,system-changes,dendrite-fork-viewanddendrite-lifecyclepassed whole. That run printed no package versions. A run in a copy of the same commit two minutes earlier, installed the same way, printed membrane 0.8.0, context-manager 0.24.1 and chronicle 0.5.0.22dfd5a59bin runs 41008114751 and 41008114750, and each passed. Build & Test ran on Ubuntu with Node 22.12.0, and three of the checks ran on macOS.405c86aaf0, this head before its merge of84e0408977, in run 40836734810, on GitHub's merge of405c86aaf0intomain4a45766f01, whose tree is405c86aaf0's. On Ubuntu, with Node 22.12.0, 24.22.1 and 26.13.0, it ran 9,365 tests each, none failing, with 63, 63 and 65 skipped. On macOS with Node 22.12.0, it ran 9,367, none failing, with 48 skipped.Changes
Line numbers here and below are this head's unless they name another commit.
AgentConfig.toolDeclarations,'head'or'entries', default'head'(src/types/agent.ts:191).nullis read as unset, and anything else is refused, naming the value's kind, never the value (assertOneOfSetting,src/config-checks.ts:37). It isn't a runtime setting, since turning it on is the host's choice from what it knows of the model.src/system-changes.ts): folding a context's changes onto a head (effectiveDeclaration,src/system-changes.ts:194), and the difference from what the agent advertises now (declarationDifference,:208).Agent.settleHead,src/agent.ts:2672), after the committing compile. It reads only the agent's own change rows (ownSystemChanges,src/system-changes.ts:159): a change compiled from stored rows whose change metadata,kind: 'system-change'with anagent, names one of the agent's own participants, its name or one of itsselfParticipants, or, for an agent that shares another's context on a branch of its own, one its source counts as its own (ownsChange,src/agent.ts:622). The framework reads that metadata from the rows the compile names as each message's sources, since a compiled message carries no metadata. A change row a host stores without that metadata is an ordinary message here. Under'entries'it goes out as stored, and no consolidation prices or takes it.moveHead,src/agent.ts:2749);withoutCarried,src/system-changes.ts:241). A change the store refuses or fails to write, a full disk included, is logged with the store's error, and the request goes on the recorded head, as it does for one that would split a call from its result; the first later request that can store a change declares what differs then (src/agent.ts:2708–2713). A row that isn't the agent's own can only take something out of what the agent declares. The request then carries the change as that compile renders it, and context-manager's last committing compile holds the same rendering. So the next compile'sfirstChangedreads the change as sent, not as rewritten (src/agent.ts:2723). It waits while the agent's own last turn has calls with no results yet, wherever other participants' messages fall (endsOnUnansweredCalls,src/system-changes.ts:32), since a change mustn't separate calls from their results. It also waits wherever a change written now would stand between anyone's call and its result, which context-manager refuses (MessageStore.refuseToolPairSplit).changeWriteCheck(src/system-changes.ts:59) is that rule over stored rows. It reads back from the end, over change rows and rows of results, to the nearest body holding a call. It reads each body whole across its shards. Each call in that body must be answered by the body's own later blocks or by a result among the rows it read back over. The agent reads the store's tail for it in blob-free windows (src/agent.ts:2289). In the residents' shared slot the call can be another resident's;copyHead,src/system-changes.ts:172), so a tool definition its caller later changes in place is declared rather than changing the head unseen, and removes the changes the head now holds only once the record is stored (src/agent.ts:2763). A head that can't be stored moves nothing: the request goes on the recorded head with its change rows, or, with none recorded, on what's current, with the agent's own change rows left out of the request and kept in the store (:2764,withoutOwnChanges), and a later request moves the head again;thinkingBinding.binds: trueeach compile is told digests of that head (bindingCompileOptions,src/agent.ts:2254). Under'entries'that's the recorded head. A declared change leaves the digests as they were. A move changes them wherever it changes what the request carries ahead of its messages, and then the compile for the new head leaves out every thinking block. After a move the compile runs again when the move removed changes or the compile was made for another head. kv-unified's hash, like the binding's digests, takes the head's tools as the wire carries them, which leaves out any field whose value is undefined and writes a Date as its text. The difference between the recorded head and the current tools compares each tool the same way (canonicalJson,src/system-changes.ts:180), so a tool whose schema holds a Date isn't declared again at every request. A request carries a copy of the head's tools, so a hook or a caller that changes what the request carries in place leaves the record as it was;previewActivation) shows the same, compiling again (dry) for another head, and stores nothing. A dry compile has nofirstChanged, so a preview shows a consolidation only once one is forced, and a dry move can't remove the rows before it compiles, so under a tight budget its window can hold less than the move's.'head', other than the subconscious's, leaves them out of its view (compiledView,src/framework.ts:2990). That covers a resident's (:16477), an ephemeral agent's, isolated (:11150) or derived (:11715), a resumed job's (:11608) and a conversation fork's (:18294). So a host's change rows take none of such an agent's budget. The subconscious's view is as it was: it already leaves out every change row not its own. A derived child always gets a filter of its own (:11715). An inspection (inspectAgentContext) opens one with the agent's view and without the'head'filter (:11572,:11608), so it shows the change rows that view keeps. The filter goes by the agent's configuredtoolDeclarations, which a manager takes when it opens (:2990–2996). While a record of the agent's head is there, or the compile holds change rows of the agent's own, the agent clears the record, and only then removes its own rows, on whichever branch is current (src/agent.ts:2686). A record the store can't clear keeps them, and a later request tries again. A later'entries'period then adopts without them. The compile runs again after the removal only when it held the rows, as the subconscious's can, and an'entries'agent's kept to its head. What the compile then commits holds what the request carries, and so do context-manager's comparison baseline and, withbinds: true, context-manager's thinking-binding record. The direct API (runInference) sends its caller's tools on the current head and leaves change rows out (src/agent.ts:1839).main's stored context injections, in either order (endsOnOwnMessage,src/agent.ts:175), so the[Continue]row, a heartbeat separator and a tool loop's prompt still follow what they follow, and membrane places the change after the next user turn.Agent.consolidation,src/agent.ts:2785): once the agent's own changes, priced from their JSON as context-manager prices a change (jsonTokenEstimator, at JSON length / 2.3), reach a tenth of the last response's reported input (fresh, cache written and cache read), the head moves to what's current at the first committing compile whosefirstChangedis at or before the first change. That compile already rewrites every change, so the move adds the head and any messages beforefirstChanged, never more than the whole prompt. A strategy that keeps changes ahead of its window (the passthrough strategies, once over budget) re-renders from the first change only at the compile where its window first passes it; after that the change stays first and unchanged, andfirstChangedfalls after it.firstChangedis null only after a start, before the manager's first committing compile, and nothing consolidates for free then.AgentConfig.thinkingBinding.binds(src/types/agent.ts:356) says whether the model binds its thinking, in three states read through two predicates (src/agent.ts:216,:220). A choice that could set kept thinking aside, which consolidation is, counts the model as binding unlessbindsisfalse(mayBindThinking). What relies on the declaration, the context manager's record, takestruealone (declaresBinding). Left out, each takes its own safe side. A move of the head that changes what the request carries ahead of its messages sets aside every thinking block in the request, and thinking fromfirstChangedon is already set aside by the compile's own change, so a model that may bind consolidates for free only where no thinking sits beforefirstChanged.forced: 'share'). The two thresholds rest on how the design priced a move. Rewriting the whole input costs its write premium, 1.15 or 1.9 times the input at the 5-minute or the 1-hour write, and repays once what requests no longer carry, the changes read at a tenth of the base price, adds up to it. At a tenth of the input that's within about 115 to 190 requests, and on models whose cache reads cost less, about 240 to 390 on Opus 5.5 and Sonnet 5.5 and 615 to 990 on Fable 5.1. A free consolidation writes only the head beyond what its compile writes anyway, so it repays sooner. The fifth is a choice we made, not a measurement. By the same model at twice the share, a forced move, which rewrites the whole input, repays in half those counts: about 58 to 95, 120 to 195 and 308 to 495 requests. The model counts every change's tokens as saved, though an added tool's definition moves into the head rather than going away, and it doesn't price the thinking a forced move sets aside on a binding model. When a request's first committing compile is refused withOverBudgetError, or withUncoveredDropErrorfor dropping messages no summary covers, while the agent has change rows of its own in its context, a derived agent's source's among them, the agent takes them out and compiles once more, beforesettleHead(src/agent.ts:2025), in every state (takeChangesOverBudget,:2730). On the head, an agent whose compile can't hold change rows takes none, leaves its record as it is, and the refusal goes on (setCompileHoldsChanges,src/agent.ts:617, read at:2734). On the head, one whose compile can hold them clears its record first: a record the store can't clear keeps the rows, and the refusal goes on. The framework tells each agent it opens on acompiledViewmanager whether that view keeps change rows: under'entries'it does, and on'head'it doesn't. The subconscious, whose view keeps its own rows, isn't told and keeps the default, that its compile can hold them. Under'entries', before a head is recorded, the removal is the head's adoption. With a head recorded, it's a move forced by the budget (forced: 'budget'). Either stores the head first: a head that can't be stored keeps the rows, and the refusal goes on. With no such rows, the refusal goes on, and on the head the record is left as it is. If the compile after the removal is refused too, the error goes on and the move stands. The compile run again after a declared change has a fallback of its own, insidesettleHead(:2724), which takes the change into the head. A forced move costs one whole write, and on a binding model the thinking the request held. It raises an operator line (head-consolidated), which names that thinking outright withbinds: true, as a condition withbindsleft out, and not at all withbinds: false. In a dry run, a move forced by the share shows in the request the preview returns, but no change row is removed, nothing is recorded and no operator line is raised (src/agent.ts:2759). The move forced by the budget is never made in a dry run (:2026).keepToHeadAfterRefusal,src/framework.ts:16414, called at the stream's error event,:27283): a request that carried a change and ended with a refusal membrane recognises (systemChangeRefused), whatever its type, or with a 400 membrane doesn't file ascontext_lengthorauth(:16417), whether or not the change caused it, goes again at once on the agent's current head. The resend takes no wait the refusal states: membrane reads a retry-after on any status, and for an agent with a home channel, as a conversation fork has, the framework records a stated wait before it asks whether the error is retryable (src/framework.ts:6893), so the resend's path doesn't ask (:27292;test/tool-declarations-failsafe.test.ts:212). A 400 of either of those types that membrane doesn't recognise as a refused change goes to the error policy, as on'head', and the agent stays on'entries'(test/tool-declarations-failsafe.test.ts:261). That leaves to the error policy one case where a resend on the head could fit: acontext_length400 caused by a host's own change rows pushing the request past the model's window, as where the agent's budget sits above that window. A resend on the head would leave those rows out. Under'entries', a change stored at a tool boundary is handed to the live stream with the round's results, and a refusal of the stream's next round counts too (src/framework.ts:17492). The agent keeps to its head until a newAgentis built for it, as at a restart. The refusal never reaches the error policy. The default policy,DefaultErrorPolicy, treats a 400 as terminal on the first failure (src/framework.ts:2541). The resend is a retry of the same turn (attempt + 1). It records no second undo checkpoint and keeps the turn's tool-call count. A resend that fails with a server error reaches the policy as the turn's second attempt (attempt1, counting from 0) (test/tool-declarations-failsafe.test.ts:220). The fail-safe decides before that rule (src/framework.ts:27300), so a re-armed send refused for its change goes again once, at once, on the head. If that resend is refused too, the turn ends with no third send and no retry, and nothing is re-armed after it, since a refusal isn't a transient failure. The resend carries no change, so the fail-safe runs at most once for each change. A resend after a 400 membrane doesn't recognise as a refused system change also raises an operator line (head-resend) quoting it, saying the 400 may not be about the change, naming the thinking the resend can set aside, asbindssays, and saying that the agent keeps to its head until a newAgentis built for it, as at a restart.framework/head-declarations/<name>. It's registered just before its first write, since a branch made from another (an undo's) carries registered states only, and a state never written holds nothing to carry. An agent that records no head, as on'head', registers none. Each is read once per branch, the branch known by its id, and kept in memory, since the framework is its only writer (src/framework.ts:16378); a branch switch (undo, redo, a rollback, a fork) reads it again. A write that would store what the state holds isn't made. A record that isn't a head is set aside, named in the log, and its agent adopts again. One that can't be read sends that agent its current head, and the log says so once a branch (src/framework.ts:16390). Under'entries'that's an adoption, whose write replaces the record and which takes the agent's change rows with it. No other agent's record changes. A write that fails is logged, and memory keeps what it held, so the next read finds the record as it was, and the agent moves nothing ("The request", above) until a later write lands (storeHeadDeclaration,src/framework.ts:16396). Going back to a release from 0.26.0 on, after an agent has declared changes, leaves their rows in the store, and that release compiles them as it does any row. Through membrane's native formatter to an adapter that sends system changes, its requests carry them as written beside the current head, a resident's carrying the other residents' too. Through any other, membrane refuses a request that carries one. A release before 0.26.0 depends on a membrane with no system changes at all.HeadDeclarationStore.writeand.deletenow return a boolean (src/system-changes.ts:11–12). The framework's store returns fromwritewhether the head is stored, and fromdeletewhether no record is left (src/framework.ts:16360,:16396). A closed fork's record stays in the store, as the rest of its data does, and leaves memory. A record is never changed in place, so a fork's and its template's, or a child's and its parent's, may be one object. An agent built without the framework, and an ephemeral agent, keep their heads in memory.src/framework.ts:2968–2984), each reading one rule for an agent's own change rows: a change row is its own when its change metadata names one of its own participants (ownParticipantsOf,:2963).residentView) leaves out held rows, another agent's stored context injections and change rows stamped as anyone else's. A change row a host stores without an agent's change metadata is in every resident's view, and goes out as stored in an'entries'resident's request. A resident configured'head'compiles without it ("On the head", above).observerView), as the subconscious reads it, leaves out another agent's stored context injections and every change row not its own, a host's included, since a change row there declares the residents' tools. It keeps held rows where its observe edge says so, as the subconscious's does (:16561).derivedView) keeps what its source's view keeps, plus its own stored context injections and its own change rows. The live shared derivation (:11705) and the reopening of an ended agent's context (reopenAgentContext,:11591) build the same views, so a fork reopened after it ends shows what it showed live. A reopened record's view follows what the record declares: one that reads the residents' slot through an observe edge gets the observer's view, and one that reads it as its own slot gets the resident's. Under'entries'those source rows are in the derived agent's requests, until it's kept to its head, so it reads them as its own change rows (ownsChange,src/agent.ts:622): it folds them into what it declares, consolidation prices them, and a move of its head, its adoption included, takes them off its own branch as it takes its own (src/agent.ts:2766, and:2742for a move the budget forces). The framework's two copies stamp them as the copy's own:deriveAgent's copy mode (src/framework.ts:11322) and a fork's seed (:18366). A shared childderiveAgentmakes already counted its parent's throughselfParticipants, and a copy-mode child's come to it stamped with its own name. The framework gives the agent their owners as it creates a shared child, from the live source (src/framework.ts:11161,:11732), and as it resumes one, from the records its view is composed from (:11518,sourceChangeOwnersOf,:11651). A child that keeps its parent's head keeps them, so the prefix they share holds, and they count toward its share, so a child carrying many of its parent's changes can be forced to move, losing that prefix for one whole write. A child's share is its own rows over its own input, so it can reach a fifth before or after its parent. In a run reported with the review ofc045b9870b, a parent whose requests carried a non-storedafterUserinjection stayed while its child, with the same rows, moved at its second request. On'head'a derived agent's compile leaves every change row out (compiledView,src/framework.ts:2994–2995), so its source's aren't sent.test/dendrite-fork-view.test.ts:203), fails with thesrcof073132e851, this head's merge of0f11975e48, apart fromsrc/tune-out/tools.ts(red first, under "Tests").selfParticipantsandpresentAs(src/framework.ts:16591–16592), whichmaincopies with the rest of the primary's config (src/framework.ts:15874at0f11975e48). So the primary's former names aren't its own participants, and its turns aren't presented under the primary's presented name.isOwnParticipantdecides whose turns are presented as an agent's own and whose change rows are its own (src/agent.ts:596). A subconscious on another model than its principal declares on its head, sincetoolDeclarationsis among the fields it doesn't take from its principal then (SUBCONSCIOUS_MODEL_FIELDS,src/framework.ts:3752). A reader fork the subconscious runs on another model than its principal declares on its head too:invokeReaderForksetstoolDeclarations: 'head'in the fork's own config when the fork'smodeldiffers from the principal's, compared exactly (src/framework.ts:16708). A reader fork on its principal's own model keeps the principal's setting.selfParticipantsandpresentAs(src/framework.ts:16493–16494), as a derived agent's record already did, and an ephemeral agent's record holds those its config names where its spec leaves them out (src/framework.ts:11180,:11186), with a spec and without one (test/dendrite-fork-view.test.ts:327). In its test, a shared child of a resident keeps, before its first request, a change its source counts as its own under a former name, and holds none once it has adopted its own head, at its end and reopened from the records alike, while its source's branch keeps it (test/dendrite-fork-view.test.ts:280), and the resident's record holds its configuredselfParticipantsandpresentAs(:302).resumeAgentreopens the context with the mapping the resumed agent runs with, an override'sselfParticipants, else the record's (src/framework.ts:11501), so its view and its head read one mapping. It gives the resumed agent a head store of its own in memory (:11520), as every ephemeral agent has. A test shows ahead:trace after a resumed job's first request (test/dendrite-lifecycle.test.ts:702).src/framework.ts:18271,:18284), so a head the template moves meanwhile costs the fork one declaration. The template's own change rows are copied as the fork's own, and a host's change row as no agent's own (:18366).createEphemeralAgent(config, spec)(src/framework.ts:11072), whenspec.inheritnames a running agent that declares entries, starts the new agent with that agent's recorded head, read as the creation begins (:11092–11093). An inherit at a checkpoint the caller names (atSequence) gives none, since the head at that point isn't recorded, and so does an agent on its head or not running. The new agent then adopts at its first request, and a shared child's adoption takes its source's change rows off its branch, so its request carries what its head holds once (test/tool-declarations-framework.test.ts:402).deriveAgentcreates its child this way, and its copy mode stamps the parent's own changes as the child's (src/framework.ts:11322). A host that copies another agent's messages itself passes a spec whoseinheritnames that agent, and stamps the change rows it copies with the metadatasystemChangeMetadata(config.name)returns. The unreleasedheadFromoption, which named the agent by hand, is gone.deriveAgentmakes counts its parent's change rows among its own, sincederiveAgentmakes the parent one of itsselfParticipants. Taking them out, at a request on the head or at a move of the child's head, takes them out of the child's own branch only: the parent's stored rows and its compile stay as they were.src/framework.ts:17156). For this, each context manager's view is recorded with it, itsseesbeside its filter (:16480for a resident,:16580for the subconscious). So a host's change row stored without metadata at the subconscious's boundary is stored but not injected, as its next request leaves it out. On the head, a change row at a tool boundary isn't injected either, as no request on the head sends one. A boundary that hands the live stream only system changes leaves the next round of a reply to[Continue]following its prompt, as a compile at that point reads it (:17231).isConversationalRow,src/framework.ts:473): it starts no re-send count again undermain'sexhaustedTurnRearmMsand clears no earlier deadline cut of a re-armed turn (:30955), counts as nothing heard (:22559), moves no turn to a channel (:22286), isn't where a turn's input pin starts (turnInputs,:1686), isn't new input to the turn when it's handed to the live stream at a tool boundary (:17220), and isn't announced as someone speaking by the notice for a message addressed elsewhere (:17298)./undo Ncounts every message but change rows, takes a change among them with them, reports N, and reports as the last visible message the last conversational one (src/framework.ts:14504);/undo N's count (indexBeforeLastMessages,src/framework.ts:1243) andisNewestIn, below, read the store's tail throughscanFromTail(src/stored-removal.ts:20), aslocateMessageIndicesand the change guard do. It reads newest first, in blob-free windows of 16 rows doubling up to 500, until the reader is done;isNewestIn,src/framework.ts:30919), and a write still deferred for the agent counts as a message after it only if it isn't a system change (src/framework.ts:30828). Inmainate85c57db9a, the check's query asks for the newestNEWEST_SCAN_ROWSrows, 32 (src/framework.ts:18706and:26712at that commit), and this pull request takes that limit out;src/framework.ts:13852);[system change], and in raw format the block without what it declared (src/modules/history/index.ts:2455), asextract's description says, since in the shared slot it may be another resident's, whose tools and system text are its own.isSystemChangeMessageandsystemChangeMetadataare exported from the root, for a host's own readers of stored rows and its own forks (src/index.ts:8).head:declared,head:movedandhead:resend(src/types/trace.ts:123).selfParticipants' docs saying it also decides an agent's own change rows (src/types/agent.ts:443,src/dendrite/types.ts:230),createEphemeralAgent's doc on whatspec.inheritgives,thinkingBinding.binds' three states in one paragraph, the README's Agents section, and the changelog fragmentschangelog.d/tool-declarations.added.md,.breaking.md,.changed.mdand.fixed.md. The settings' doc and the README say that on'head'the compile leaves change rows out, that a host's own change rows are the host's to remove, which 400s keep an agent to its head, and how system text is compared: block by block, in any order, so a block whose text changes every request moves the head every request (src/types/agent.ts:152–160).main'sexhaustedTurnRearmMsdoc now says that a system change isn't a message said to the agent, the.changedfragment says such a change joins what 0.29.0's re-arm entry lists as not a message said to the agent, andsrc/silent-heartbeat.ts' doc names system changes beside stored context injections.Who needs to act
toolDeclarations: 'entries'for agents on models that take mid-conversation system messages through membrane's native formatter and an adapter that declaressendsSystemChanges, and setsthinkingBinding.bindswhere it knows whether the model binds its thinking.'entries'saves writes only while the system blocks already declared stay as they are: a block whose text changes every request, such as one carrying a timestamp, moves the head every request.mainloses them on every agent on'head', the default, and setstoolDeclarations: 'entries'for the agents that should send them. The changelog files this as breaking (changelog.d/tool-declarations.breaking.md).'entries'the agent neither folds them onto its head, consolidates them nor removes them, and an agent configured'head'leaves them out of its request, and out of its compile where the framework opens its context manager.sendsSystemChanges. membrane's documentation says a request carrying a change through a wrapper that doesn't forward it is refused before sending, and such a refusal falls to the fail-safe.On
'head'tooWhere an agent on
'head', the default, has change rows in its context, such as a host's, these change what it does:main's requests carry a host's change rows as stored;/undo N's count and the last visible message it reports, and the poisoned-history shed, pass over change rows, and history's tools show a change as[system change]("Other readers of stored messages", above).Whatever its context holds:
mainat48f26ce56creads only the newest 32 rows (src/framework.ts:29871at that commit; "Other readers of stored messages", above);copyHead,src/system-changes.ts:172, atsrc/agent.ts:2170), wheremainsends the tools it's given: a value JSON can't write, such as a BigInt, now fails as the request is built rather than as it's sent, and a field JSON leaves out, such as a function, is left out of the request as it was of what was sent;headHash,src/agent.ts:182), wheremainhashes the tools it's given (src/agent.ts:1920–1925at48f26ce56c);selfParticipantsandpresentAs("A resident's registry record", above);selfParticipantsandpresentAs("The subconscious", above).With
mainThis head has merged
maintwelve times, and this section covers the last six.073132e851merges0f11975e48, which brings 0.30.0's agent registry (#546), #881, under which astop()ends its framework instance, and #378's progress bounds. Git reported conflicts insrc/agent.ts,src/framework.tsandsrc/index.ts, and the merge's message names how it resolves each.405c86aaf0merges4a45766f01,mainsince 07:26 UTC on October 29, which brings #869, the subconscious's own window and thinking binding. Its conflicts are insrc/agent.ts, where the agent's settings check now callsmain'sassertThinkingBinding(src/agent.ts:198), whose words forbindsare this pull request's three states (:207–212), and insrc/framework.ts's imports, where it takes both sides'.7d78b4bb36merges84e0408977,mainsince 16:40 UTC on October 29, which brings #871. Under it, a re-armed turn's send cut by a deadline marked as run out counts as a transient failure, unless it follows another such cut with no transient failure, answered round or message said to the agent between them. Git reported conflicts insrc/framework.tsandsrc/types/framework.ts, and the merge keeps both sides of each. At the stream's error event, the resend on the head after a refused change still decides first, and then #871's rule for a re-armed turn (src/framework.ts:27300–27302). InnoteSpokenTo, the return for a row that isn't said to the agent comes before #871's clear of an earlier deadline cut (:30955–30956), so a change row clears no cut.exhaustedTurnRearmMs' doc takes #871's text, with system changes among what isn't a message said to the agent (src/types/framework.ts:392).cfb44201dbmerges48f26ce56c,mainsince 02:48 UTC on October 30, which brings #867's turn-input pin, and git reported no conflicts.559a158d89merges4429afe2b6,mainsince 23:40 UTC on November 3, 251 commits past48f26ce56c, which bring #938, under which the agent is told nothing of reasoning a compile leaves out or a provider drops, and #975's event suppression. Git reported conflicts insrc/types/agent.ts, wherethinkingBinding.binds' doc keeps this pull request's three states with #938's sentence in place of the bullet saying the agent is told, and insrc/framework.ts, in the mid-turn drain's comment and in the residents' and the subconscious's views. The merge's message names how it resolves each.76e2d43e48mergesa25e38b1bf,mainsince 01:59 UTC on November 4, which brings #969's locked prose routing. Git reported one conflict, insrc/framework.ts, where a boundary that can't re-pin notes what it stored as arrivals: the resolution keeps this pull request's call, which passesnoteArrivalthe stored row so that a change row isn't taken as conversational, withmain's added comment (src/framework.ts:17376). Where the merges meet this pull request:src/framework.ts:27283,:27300). Otherwise, if no provider hold has taken the turn's retry (:27287) and no stated wait is held (:27293–27296), the error policy is asked withcuts ?? attempt(:27302), unless a stated wait has failed ormain's rule for a re-armed turn ends the turn (:27301).cutsis feat(inference)!: cut an AgentFramework model call that stops making progress, and give a deadline expiry one fresh call #378's count of a deadline's tries when the error is a deadline's expiry, and undefined for any other error (:27299).:27303–27304keep that count whencutsis defined and the turn is retried, and clear it otherwise.src/agent.ts:1839).createEphemeralAgenttakes the registry's spec second, as 0.30.0 released it, and the spec'sinheritis how a new agent continues another's context ("An agent that continues another's context", above).turnInputs, feat(framework): keep the rows that open a turn raw while it runs #867's, passes over a change row (src/framework.ts:1686), as this pull request's other readers of the store's tail do.releaseAgentRuntime,main's release of an ended agent, now also drops the recorded head's entry in memory (src/framework.ts:12283). The store keeps the head.RESIDENT_VIEW_FILTER(src/framework.ts:2957), which feat(gate): suppress rules keep an agent's chosen events out of its context #975 made leave them out, and the subconscious, which reads held rows, leaves them out throughobserverView(:2974), as feat(gate): suppress rules keep an agent's chosen events out of its context #975's own filter for it did.viewHidesSuppressedasks an agent's view throughstoreViews, the map of views this pull request keeps (:38320).prose_locustool joins an agent's tools while the agent is under locked prose routing (src/framework.ts:8777). Under'entries', its arrival or removal is declared as any change to the agent's tools is.What each part is for
deriveAgentpresents its parent's turns as its own and starts on its parent's head, so the parent's changes are part of what it declares. Counted by its name alone, they would be declared again. The views, the head's folding, the removal, a fork's seed andderiveAgent's copy read one rule, so an agent never folds or removes a row its view hides, and its view hides none of its own. A stamp is read only from change metadata, so a host's row whose other metadata happens to name an agent stays in every resident's view.thinkingBinding.bindsin three states: it's one fact about the model, and two features read it, each needing a different safe side where the host hasn't said. Consolidation that counted an undeclared model as not binding would set aside its kept thinking; the context manager's record turned on for every undeclared model would change every host's compiles. Two predicates, one place, so neither feature derives its own reading.'entries'the request carries the recorded head, so digests of what the agent advertises now would read every declared change as a new head, and the record would leave out every thinking block at each one, the cost declaring changes exists to avoid. A move compiles again whenever the compile was made for another head, and the hash takes the tools as the wire carries them, as the digests do, so the request, its compile and the stream's notes are of one head.'head'agent's compile: it sends none, so pricing them counted against its budget what it never sent. Nothing in the agent removes a host's change rows, and context-manager drops no change row (context-manager 0.24.1'ssrc/system-changes.ts, its module doc), so they could fill the budget until every compile was refused. A helper for a host to remove its rows, or a rule for when an agent may fold a host's row, would each be a new promise to hosts, so a host's rows are the host's to remove, as the setting's doc says. The filter goes by the configuration because context-manager keeps one filtered listing per listing and predicate, taking the predicate as a pure function of the message (context-manager 0.24.1'ssrc/message-view.ts:36–40), so a filter can't follow the fail-safe's state. A derived child gets a filter of its own because, given none, context-manager gives it its parent's (context-manager 0.24.1'ssrc/context-manager.ts:1385), which would hide an'entries'child's own rows under a'head'parent with no view.'entries'period could then declare a tool after the calls that used it.main's rule for a re-armed turn: a 400 means the provider answered, and membrane's refusal before sending means the request never reached it, so neither is an outage, and the resend corrects the framework's own request.main's rule keeps a re-arm from adding sends into an outage, and the resend carries no change, so the fail-safe adds one send to a re-armed turn, on the head.bindsleft out the agent can't know a move set any thinking aside.seesalone, the boundary would inject a row the filter leaves out. On the head no request sends a change row, so the boundary injects none. A round handed only system changes keeps its prompt because a change isn't a turn: a compile at that point reads the prompt past it, so the live round and a compile there agree.noteSpokenToandnoteArrivalkeepsees: they count only rows that are said, which a change row never is, and each view's filter differs from itsseesonly on change rows (src/framework.ts:2971,:2978,:2983).spec.inherit: a fork's first request then declares only its own difference, not its whole tool set, and reads its parent's cache rather than writing its context fresh. 0.30.0'sspec.inheritalready says a new agent continues another's context, so it gives a host's subagent the same without exposing heads in the public types, whichheadFromdid by hand. The head is read as the creation begins, so it's never newer than the rows the new agent continues: an older head costs one declaration, and a newer one would carry a change twice. A checkpoint the caller names gets no head, since the head there isn't recorded. The record is the template's or parent's head itself, with no copy of its own, since no record is ever changed in place. Each move records a fresh copy, and a request carries one. A closed fork's record stays, as its other data does. The fork's name isn't used again.'entries'for a model that takes system messages mid-conversation ("A host that wants it", above), and a subconscious on another model may run on a model that doesn't, so it doesn't take its principal's setting. A reader fork is a new agent made from its principal's config, so it sets the field in its own config. One on its principal's own model sends the principal's recorded head and its change rows (test/tool-declarations-framework.test.ts:1049), so it keeps the principal's setting.selfParticipantsandpresentAsare an agent's identity, andcreateSubconsciousAgentgives the subconscious its own name. With the primary's, the primary's former names were its own participants, so their change rows were its own under the rule above.head:trace follows a resumed job's first request, and that test fails./undo N's count and its last visible message, the batched wake's newest entry, the shed, the unanswered-calls guard and the request's last message: each reader of stored messages that counts or ends on turns reads past change rows, which aren't turns. The tail scan is one helper, so its window and its blob-free reads are in one place. The guard also finds the agent's own calls past other participants' messages and matches results to them by id, so another resident's message, or another call's result, doesn't let a change in between a call and its result. It counts a result in the call's own message, as context-manager's check does (src/system-changes.ts:41). It reads the agent's own name alone, not itsselfParticipants: a child derived while its parent's call is open inherits that call, whose result never reaches the child's branch, so reading the parent's turns as the child's own would hold the child's declarations until its first turn of its own, and its first turn would go without the tools declared since its start (test/tool-declarations-framework.test.ts:540).^0.24.0always has one. The guard copies context-manager's rule, so a write the store refuses all the same goes on the recorded head, declaring nothing, and logs the store's error: a difference between the two rules defers a change rather than failing the turn, and a test builds each of the rule's table cases in a realMessageStore, so a difference that changes the answer for one of those cases fails CI.'entries'its compile still holds them, so they count against its budget, as they do for a compile kept to its head (src/types/agent.ts:182–186).rearmedharness runs a resident's first turn, changes its tools, and fails the second turn's send with a 503, which its error policy doesn't retry, somain'sexhaustedTurnRearmMssends the turn again. Each later send fails with the error the test chooses, if any, and the policy retries aninvalid_requestfailure, as a refusal is.main's tests of a turn's channel and of held writes (test/shared-channel-id.test.ts,test/deferred-window-only.test.ts) pass a row, asrepinChannelandnoteStorednow take one.turnsharness runs a resident's first turn, changes its tools, and fails the second turn's requests with the error each test chooses, if any, through a stream that ends in membrane's error event. A third turn follows when a test asks for one, and a spy error policy records each call's attempt. The test's error constructors stand for a provider's 400 that membrane recognises as a refused change or doesn't, membrane's own refusal before sending, and a failure that isn't a 400.changeRowand the fake context'sgetAllMessages,getMessageCountandgetMessageWindowgive the agent-level tests stored change rows.branch, sincemain'snoteRenderedInjectionskeys the stored injections a compile rendered by branch.windows, with each one's offset, limit andresolveBlobs) shows the guard's read stays at the tail, blob-free.firstChangedwhen the property is read (a getter,test/tool-declarations.test.ts:44), as context-manager's is. The getter counts its reads, and with'compare'it compares with its last committing compile as context-manager compares.priceOfprices a change with context-manager's own estimator.digestdigests a value as the agent does (astest/thinking-binding-binds.test.tshas its own), so the identity tests compare the digests with what the request carries.Assemblingnames what the assembly returns beside its request.content, as every stored row does.Tests
test/system-changes.test.ts:MessageStore, which refuses a change at its end, in its own words, exactly where the rule says one would split a call (:198);test/tool-declarations.test.ts:'head'as before, with change rows left out and an earlier period's record and rows removed, with or without a trace callback, and a record that comes back (as a branch switch brings one) removed again;head:declared's fields;[Continue]case; a tool loop's prompt past a trailing change;:310); a change stored by an assembly whose request never went out;:417,:441), and with none recorded sending what's current without its own row that adds a system block (:451); a change row the store refuses, the request going on the recorded head with nothing declared and the refusal logged once with the store's error (:463); a move taking only rows that hold a change; the recorded head kept as recorded when what a request carries is changed in place, a nested schema included; the guard reading one blob-free window of the newest rows;:301); a response that reports no input leaving the last real one, so nothing is forced (:1022); a consolidation free only at a compile that re-renders from the first row a move takes, a derived agent's source's included (:1031), and a derived agent holding only its source's rows consolidating by share (:1052);NativeFormatter: the change goes out as a system message after the user turn, the[Continue]one included;firstChangednot read below a tenth; a declared change read as sent at the next compile, so no free consolidation follows it, by a stand-in whosefirstChangedcompares with its last committing compile; and a binding model's thinking, beforefirstChanged, at it, and redacted;firstChangedafter the change, null, and a binding model's thinking before it; a compile refused over budget with the agent's own rows kept, with a head recorded, before one, and on the head, a compile refused for dropping messages no summary covers (:832), a derived agent's compile refused over budget with only its source's rows kept (:816), a derived agent kept to its head taking its source's rows out, also when its compile is refused over budget (:801), and under kv-unified the compile after the move hashing the head that goes out; the compile after a declared change refused over budget, which the head takes the change into, compiling once more for the new head; the refusal going on with nothing recorded and no change kept, no change kept, a second refusal (the move standing), another failure, a dry run, a head that can't be stored, with the change kept (:852), on the head while the record can't be cleared, with the agent's own rows kept (:489), and on the head with no rows, the record left as it was (:499);:580);:548,:558), not where a consolidation's changes start, and on the head left out of the request with nothing traced;thinkingBinding.bindsin three states; withbinds: trueunder'entries', each compile's digests are those of the system text and tools the request carries, kept by a declared change and changed by a move, a replacement with no change row to take included, and the stream's binding is the compile's the request was built from; a dry move compiled again for a head that differs from the record only in a field set to null in the record and left out of the head, with the compile told the digests of the head the preview shows;test/tool-declarations-framework.test.ts:/undo N's count and its refusal, and the last visible message it reports when a change follows it;bindsleft out and outright withbinds: true, naming none withbinds: false, and for a move forced by the budget;binds: trueand the drop control, a declared change keeping a reply's thinking before it, and a replacement and a forced consolidation each leaving it out at the compile for the new head;'entries'period, the agent's record and its change row gone, and a noted reply after keeping its thinking (:209); kept to its head after a refusal, its compile run again once its own change row goes, and a noted reply after keeping its thinking (:240);:806);'head'agent's compile leaving every change row out (:1179): a resident's, an isolated ephemeral agent's, a host's shared child's and a conversation fork's request going out beside a host's change rows that alone exceed its budget; an'entries'child of a'head'conversation fork keeping its own change, declared once; an agent kept to its head keeping its own change rows while its record can't be cleared, and taking both out once it can; and an inspection of a live'head'resident showing a host's change row its compile leaves out;createEphemeralAgent'sspec.inherit:atSequence), or from an inherit naming no running agent;:361), and a dry move of a host's shared child showing none of its parent's rows, under either of its parent's names (:386);:344), and one at a checkpoint (atSequence) with its parent's prompt, tools and added block sending the block once (:402);:540);deriveAgentmakes, shared and copied, starting on its parent's head, its first request carrying the parent's prefix and declaring nothing;deriveAgentcopies, taking its parent's copied change out of its context when its head moves;selfParticipants, kept in its view and counted as declared; a host's row whose metadata names an agent but isn't change metadata, in every resident's view; and the subconscious taking none of the primary's identity;:1038); on its principal's own model, its request carrying the principal's change as stored (:1049); and of a principal on its head, on that model, declaring on its head too (:1058);isSystemChangeMessagefrom the package root;main's stored context injections, past the two the other way round, and past an injection and then the change a dry run appends, each with its[Continue]last;[Continue]last though a host's change row follows it, and the same for a resident kept to its head (:747);test/dendrite-fork-view.test.ts,main's file::167);'head'child holding its source's rows and its own (:543), a'head'child at a checkpoint, run to its end and resumed (:564), and a'head'resident holding its own (:590), each compiling once when its compile is refused over budget and keeping its rows;:479), and a copy-mode fork of a host's running shared child stamping them as its own, so its move takes them (:496);:344);selfParticipantsandpresentAs, with a spec and without one, recording them (:327);'head'job, in a namespace of its own and continuing a resident's context, sending its request beside a host's change rows that alone exceed its budget (:395).test/dendrite-lifecycle.test.ts,main's file: a resumed job reports its head as any agent does.test/present-while-acting.test.ts,main's file: under the default locus rule, a system change stored mid-turn with an addressed message's metadata is no one speaking, so nothing is announced, and the tool boundary hands it to the live stream.test/batched-wake-line.test.ts: a DM is still the newest entry with change rows (the agent's, another agent's and one unstamped) and clock rows after it, past more than 32 such rows too, and isn't once a message follows them. It's still the newest with a system change deferred for the agent, and isn't with a deferred message.test/stored-removal.test.ts: the tail scan visits every row newest first with its index across window boundaries, reading no blobs, in windows of 16 rows doubling to 500, stops where its reader is done, and reads nothing in an empty store;locateMessageIndicesfinds ids across windows and stops reading once all are found.test/tool-declarations-failsafe.test.ts: a resident whose tools change between turns, with the second turn refused: a recognised refusal, and membrane's own with no HTTP status, resend on the head as the same turn (one undo checkpoint per turn, the error policy never consulted) and the next turn goes out on the head; a failure of the resend reaches the policy as the turn's second attempt (attempt1, counting from 0); a 400 membrane doesn't recognise resends and raises the operator line, binding or not, saying the thinking's cost withbinds: trueand none withbinds: false, with its HTTP status; a 500 and a refused request without a change go to the error policy; and each turn's latest response's whole input reaches the agent. In a turnmain'sexhaustedTurnRearmMssent again: a re-armed send refused for its change resends once, at once, on the head, the resend answers the turn, and the error policy sees only the 503 before it; a resend refused again ends the turn with no third send and no retry, under a policy that retries a refusal, and nothing is re-armed after it. A turn whose first request carries no change, with a host's change stored at its tool boundary and the round after it refused, resends once at once on the head, without the error policy, and the turn ends well. A refused change's resend goes at once on the head though the host's provider hold would hold a retry (:205). A conversation fork's resend after a refusal that states a wait goes at once and records no wait, with the registry answering for the agent as it does for a conversation fork (:212). A 400 membrane files ascontext_length, and one it files asauth, on a request carrying a change, each go to the error policy with no resend, and the agent stays on'entries'; each marked as a refusal membrane recognises resends on the head (:261,:269).test/turn-inputs-pin.test.ts,main's file: a host's change row stored between two turns, where the second turn's pin starts at its first input past the change row (:272).test/mid-turn-injection-as-compiled.test.ts,main's file: a host's change row stored without metadata at a resident's tool boundary goes into the live turn, and the next request sends it, under'entries', and neither on the head; at the subconscious's boundary such a row is stored but not injected, and its next request leaves it out, with its principal on'entries'or on its head.test/silent-heartbeat-rows.test.ts,main's file: a later round handed only a system change at its tool boundary keeps following[Continue], as a compile there reads it.test/continuing-start-silence.test.ts,main's file: a system change a host stores during a provider's hold leaves the turn's line held; under'entries', a system change handed to the round after a send leaves the retry's line held.test/shared-channel-id.test.ts,main's file: a system change moves no turn's locus, whatever channel its metadata names.test/exhausted-turn-rearm.test.ts,main's file: a host's change row stored without metadata, among the rows that don't start a resident's re-send count again, and one written for the subconscious not starting its count again. Two cases store a host's change while a re-armed send's deadline cut is reported, with the cut at the send's start and reported by its stream, and in each the resident's context holds the change and the next cut still ends the re-arming (:538).dc8800379b, in its run above: withd44fc03d29'ssrc/agent.ts,tool-declarationsfails only the refused-row test, at its assertion that the refusal is logged.d44fc03d29, in its run above: with5e4a561cc3'ssrc/agent.ts,tool-declarationsfails the two new tests, the failed write's at an assertion and the refused row's with the refusal its stand-in for the store throws, andsystem-changesnone. In those two files, loosening the guard's copy fails both rule tests andtool-declarations' wait test, and taking out either fix fails only its own test. Turning off context-manager's own refusal in its installeddist/src/message-store.jsfails only the real-store test insystem-changes, the one file that run covered.d61f9ea340, in its run above: withc045b9870b'ssrc/agent.tsandsrc/framework.ts,dendrite-fork-viewfails the three'head'tests, each at an assertion, andtool-declarationsandtool-declarations-frameworknone. Taking the fallback's guard out fails the same three; not telling a host's child, a resumed agent or a resident fails only its own test in those three files.fe9b88bdcc, in its run above: with48ede2f824'ssrc/agent.tsandsrc/framework.ts,tool-declarationsfails 3 tests (the child kept to its head, the free consolidation's check and the source-only share),tool-declarations-framework1 (the child on its parent's head) anddendrite-fork-view1 (the copy-mode fork), each at an assertion. With the dry filter, the resume's owners, or the resume walk'sselfParticipantsor depth each broken in turn, as the review of48ede2f824gave them, only its pin fails. With the fold, the copy's stamping or the rows a removal takes put back on the agent's own participants, only the tests predicted for each fail.48ede2f824, in its run above: with2289aef75a'ssrc/agent.tsandsrc/framework.ts,tool-declarationsfails the uncovered drop with the refusal it fixes, which reaches the test as the request's error,tool-declarations-framework3 (the forced move's operator line, the host's shared child's move and the checkpoint child) anddendrite-fork-view1 (the former-name test), andtool-declarations-failsafenone. The derived agent's budget move fails too, since2289aef75a's code has nosetSourceChangeOwners, so that arm's build exits 2 on that error and still emits. With each of the four breaks the review of2289aef75agave applied, its pin fails and no other test in those three files; withonHead ||taken out ofstatedWait, only the conversation fork's test fails, its resend not ending within the test's 10 seconds; with the budget fallback counting the agent's own rows alone, only the derived agent's budget move fails.af040831ff, in its run above: with76e2d43e48'ssrc/system-changes.ts,src/framework.tsandsrc/agent.ts,system-changesfails 2 tests (the damaged row and the Date),tool-declarations1 (the cycle the agent's own message answers),dendrite-fork-view1 (the job's recorded mapping) andtool-declarations-framework1 (the head store's log line, which now says what follows). With the agent'sselfParticipants' turns read as its own in the open-call guard,tool-declarations-frameworkfails only the shared child derived while its parent's call is open.2b0c223934, in the run above: with thesrcofcfb44201db, this head's merge ofmain48f26ce56cbefore the commits answering the first review,tool-declarations-frameworkfails 7 tests,tool-declarations5,tool-declarations-failsafe2,turn-inputs-pin1 anddendrite-fork-view2, each of them a test those commits add, and the rest of those files' tests pass.2b0c223934and rebuilt, in the same run, each failing exactly the tests predicted for it before the run:'head'agent's compile: change rows kept in every'head'filter fails the four tests intool-declarations-frameworkof a request beside a host's change rows that alone exceed the budget, the inspection test, and both resumed jobs' tests indendrite-fork-view. The'head'filter taken out of a resident's manager fails the resident's test and the inspection test, and taken out of an isolated ephemeral agent's or a conversation fork's, that agent's test. A derived child given its view's filter alone, and none where it has no view, fails the shared child's test and the'entries'child's, and given the'head'filter only where it has a view, the'entries'child's. A resume opened without the agent's setting fails both resumed jobs' tests, and an inspection's derivation left without a filter of its own fails the inspection test.tool-declarationsandtool-declarations-framework, and the framework's clear reporting no record left whatever happened fails the latter. Under a compile refused over budget, rows taken out with the record not cleared fails "lets a refusal over budget go on, keeping its own change rows, on the head while its record can't be cleared", and the record cleared before checking for rows fails "lets a refusal over budget go on as it was on the head, its record kept, with no change row of its own to take".turnInputs: acontext_length400, or anauthone, kept to the head fails its own test, andturnInputscounting a change row fails its test.a91ad6d5d2, this head before its merge of48f26ce56c: with598f8cc58c'ssrc/framework.ts,tool-declarations-frameworkfails one test, "on another model than its principal sends no change row, and the principal's change stays where it was", and passes the rest. Six pieces, each taken out alone and rebuilt with one test file run, each fail exactly one test in that file. Three arewithoutCarried's: its filter of removals a host's row already carries fails "doesn't declare again a removal a host's row already carries" intool-declarations, and both its subtraction of system text such a row already adds and its count of each further copy fail "doesn't declare again system text a host's row already adds, and declares each further copy the agent holds". A reader fork on its principal's own model set to'entries'whatever the principal's setting fails "of a principal on its head, on its own model, declares on its head too" intool-declarations-framework. A resident's record without itspresentAsfails the former-name test indendrite-fork-view, and a resume that reopens with only an override's mapping fails "gives a child resumed with no mapping of its own the view of its record's" there too.405c86aaf0, this head before its merge of84e0408977: with thesrcof073132e851, this head's merge of0f11975e48, except forsrc/tune-out/tools.ts, which was405c86aaf0's,405c86aaf0's tests fail 22 tests: 14 intool-declarations-framework, 4 indendrite-fork-view, and 1 each indendrite-lifecycle,present-while-acting,tool-declarationsandsystem-changes. Thesystem-changesone callsownSystemChangeswith a predicate, where thatsrctakes a name.continuing-start-silence,shared-channel-id,silent-heartbeat-rowsandtool-declarations-failsafepass whole.7d78b4bb36's resolutions, each taken out alone at67ba70e53dand rebuilt:exhausted-turn-rearmfail.noteSpokenTojudging a row by its metadata alone: six tests inexhausted-turn-rearmfail. They are the two new cases, the three cases of "a turn whose retries ran out on a server error is sent again after each delay, once each time, until the delays run out", and "a host's system change written for the subconscious, which its view leaves out, doesn't start its count again".405c86aaf0and rebuilt, and each fails a named test:sees, fails the shared-fork test and the resumed child's view. A reopen that composes only a shared-slot derivation, a reopened observer given the residents' view, or an observe edge'sincludeHeldignored fails the subconscious-fork test. A resident's record without its mapping fails the former-name test, and a resumed view read from the record's mapping fails the resumed child's view.selfParticipantstest, a stamp read from anyagentfield fails the change-metadata test, and the subconscious keeping the primary's identity fails its test.tool-declarations. Others' rows counted into the difference fails the host's row in the residents' slot, the system-text row, and "counts only its own change rows".inheritfails eight tests, a head despite a checkpoint the caller names fails theatSequencetest, and the template's head read after the seed's awaits fails the fork's head test. Without the release's drop of the recorded head from memory, the fork-from-template test fails, andderiveAgent's copy without the stamp fails the copy child's head-move test.0150d3b453, before 0.29.0 was merged in, and rebuilt, failed a named test, or left one that never ended (on Linux 6.12 x86_64 with Node 24.22.1, context-manager 0.23.1 and membrane 0.8.0). The pieces, as their breaks:'entries'; the shed's check for a row holding a system change taken out.isNewestIn: a 32-row bound on its scan; counting a row the agent's view leaves out; counting a system change.main's rule for a re-armed turn; that rule left out at the error event; the resend not kept to the head.seesin place of the agent's view filter; the boundary injecting a change row on the head; the round's prompt dropped for a round handed only system changes; a change row counted as said, as heard, as new input to the turn, or as moving a turn to a channel, by its metadata.systemBlockswithoutmain's notice tag for a block that isn't text.main's rule left out at the error event, the test of a resend refused again never ends, its policy retrying the refusal. With the resend not kept to the head, the test of a resend that answers fails at "the resend went out on the head", and the test of one refused again never ends, the resend going out again each time. Unstopped, the first ran 3,357 s on9d2f62310e's files before the test runner failed its file.9c5d59f4to6eb90104, taken out or weakened alone in the source at6eb90104and rebuilt, failed a named test (65 of 65). The pieces, as their breaks:headFrom's head read before the open.isNewestIncounting a system change or a clock row, or reaching back two rows; the tail scan reading one window, starting at its full window, never growing its window or growing past its cap, never stopping, or reading blobs;locateMessageIndicesreading on once all are found; the change guard reading every window; the shed taking an unstamped change row; host undo counting a system change.bindsas declared; the record on withbindsleft out; the binding told the advertised head under'entries'; the stream told the binding of the first compile's head, or noting under the first compile; a binding head not compiled again after a move; the thinking named outright withbindsleft out, or named wherebindsisfalse.'entries'with a record; an adoption over budget traced as forced; the compile after a budget move made for the old head; a first change at index 0 not consolidated;firstChangedread below a tenth; a dry move keeping every source; the budget's move named as the share's; no fallback for the compile after a declared change, or that compile made for the old head; no compile on the head after taking the agent's own rows out.mainat4adbd600and the commits after it, each taken out alone ata2d7bf7band rebuilt, failed a named test: either half of the residents' view filter or of the subconscious's, change rows or stored injections left out of whatisNewestInlooks past, a resident's view hiding every change row not stamped as its own, the subconscious's view hiding only those stamped with another agent's name, a move taking every change row, stamped or not,endsOnOwnMessagereading past injections alone, change rows left out of the request's tail count, a dry run's appended change left out of it, andtoolDeclarationsleft out ofSUBCONSCIOUS_MODEL_FIELDS.Under context-manager's strategies
At
740f4f979d, an earlier commit of this head, before its last five merges ofmain, the reader fork's setting on another model (c77a075fb4), and the commits answering the first review and both automatic reviews, we ran one resident under'entries'with each of these from context-manager 0.24.1:PassthroughStrategy;AutobiographicalStrategy, flat, hierarchical, and with adaptive resolution and kv-unified folding, at connectome-host's standard kv-unified settings (src/framework-strategy.tsat612ec8dbe3), with their token sizes scaled by 0.02 and the cache priced at 0.1 for a read and 2 for a write; andKnowledgeStrategy. A stand-in took membrane's place, and the test file is ours, not part of this pull request. It ran on Linux 6.12 x86_64 with Node 24.22.1, installed with no lockfile, and printed context-manager 0.24.1 and membrane 0.8.0 but no chronicle version.binds: true, with a signed thinking block in every answer) and on one that doesn't: 30 runs of 32 requests. The stand-in answers each of the agent's requests with text, and reports as its input the request's JSON length over 4. It answers each request compression makes with a summary naming the stored places of the messages it was given, so each summary can be placed against the change.When the head moved for the change, by strategy and the change's size:
Summary merges past change rows. context-manager 0.24.2's autobiographical strategy merges two summaries only when no listed message stands between their sources (
src/strategies/autobiographical.ts:6672–6673there), and under'entries'an agent's own change rows are listed messages. So while they stay in place, summaries on either side of each don't merge. At48ede2f824, an earlier commit of this head, we ran one resident under the hierarchical strategy with a context budget of 12,000 tokens for 200 turns, each response reporting its request's characters over 4 as its input, against the same run on'head'. In each run the rows' share reached a fifth and forced a move:'entries'against'head'By reading, the input can't pass the budget plus the head, so the rows force a move by the time they reach about a fifth of that. These are characters, not cost: a larger request is mostly read from the cache, each merge rewrites the prefix from the merged summary on, and we priced neither. The framework prices change rows at characters over 2.3, so on a real provider the turn where a fifth falls will differ.
On Anthropic's API
At
2b0c223934, we sent one agent's requests to Anthropic's Messages API, once withtoolDeclarations: 'entries'(E) and once with it unset (H). connectome-host ata1ef80ae, with one line added to pass the setting, ran the agent for four turns through its Anthropic path, on a package built from2b0c223934beside the membrane 0.8.0, context-manager 0.24.0 and chronicle 0.5.0 its install gives, on Linux with Bun 1.4.4. A stand-in for the API answered and recorded each request, and the agent's MCP server added a tool after the first turn. We then sent the agent's four recorded requests in each arm, in order, toclaude-opus-5-5, each as recorded but with streaming off,max_tokens16 and itsthinkingsetting left out. All 8 returned 200. What each turn was billed, as input tokens uncached, written to the cache and read from it:toolsand reads none.Not verified
NativeFormatterwithout sending them. The run on Anthropic's API declared one change, through one provider, with its replies from a stand-in. No request to a provider moved the head, consolidated or met the fail-safe.'entries'agent the fail-safe keeps to its head. No test or run has one.compiledView(src/framework.ts:2990–2996) gives a manager the'entries'filter by the agent's configuredtoolDeclarations, which keeps them.src/framework.ts:18399).src/framework.ts:18394). No test has one.src/framework.ts:27299setscutsto feat(inference)!: cut an AgentFramework model call that stops making progress, and give a deadline expiry one fresh call #378's count, which:27302gives the policy in place ofattempt.:27285–27287say "A hold takes the turn's retry" and check for a hold before the policy is asked.'head'template, or a childderiveAgentcopies from a'head'parent, carries of its source's change rows. No test has one. Each copies from its source's compile (src/framework.ts:18363,:11321).WindowedPassthroughStrategy, which the subconscious uses (src/framework.ts:16553).Effie-2756 wrote the code. Ada-1017 decided its shape, including the resend after any 400, the forced moves, which came out of a design pass Phoebe-2419 led, and
thinkingBinding.binds' three states. She also chose to have the fail-safe decide beforemain's rule for a re-armed turn, and the shape of the fix in53954c3f20. For this catch-up she decided the views for each kind of reader, the starting head fromspec.inherit, an agent's own change rows by its participants, and leaving out what the request already carries. Oscar-1005 shaped how it meets membrane and context-manager, and Agnes-1009 asked forheadFrom, which this head replaces withspec.inherit. Ezra-2423 read the code, ran the agent for 400 turns under context-manager's strategies, and weighed what each part is worth. Gladys-2413 and Desmond-2579 read every commit from the fixes answering RevEngine's review throughd5bad56df0, the merge with #785 included, and Desmond found that the binding's digests had to be of the head the request carries. Desmond and Gladys read the merge ofmain,9d2f62310e, and Desmond read the merge of 0.29.0. Each found by reading a path by which a host's change row stored without metadata acted on the subconscious, whose view leaves the row out: Desmond's into its live turn at a tool boundary, Gladys's into its re-send count and what it has heard.53954c3f20closes both, and Desmond read its changes tosrc. Our internal review read each round througha2d7bf7b, and then the changes since it through the view fix, with how each merge ofmainresolved its conflicts. Ada chose which of its findings to fix, and this head fixes five of them. Desmond read the three commits that fix three of them,9e07b8bc0c,8c9bfeff01and0150d3b453. Gladys-2413 read the merge ofmain,073132e851, with how it resolved each conflict, and the changes from it through67ee0895e0. She found that a context reopened from a resident's record needed the resident's mapping, which67ee0895e0adds. Desmond read the changes740f4f979dmakes, before its one later edit, which moved a function so that a doc comment sits over the function it describes, as he found it should. He also read the test1930df5d07adds, in a version with a longer title, and how405c86aaf0resolved its conflicts. He read how7d78b4bb36resolved its conflicts, in a version before one doc line was rewrapped as he suggested, and the test after it. Our internal review read the merge ofmainwith the three commits after it, then the changes through67ee0895e0, and Ada chose which of its findings521e6f02dband740f4f979dfix. Phoebe-2419 found that a reader fork on another model kept its principal'stoolDeclarations, and readc77a075fb4andb5d3e41ce0, whosetoolDeclarationsdoc she corrected. Our internal review read the changes from740f4f979dthrough the reader fork's fix, and twelve of its findings are fixed inc77a075fb4's tests and message and inb5d3e41ce0.For the findings of the first review by @Anarchid, Ada-1017 decided how each is answered: a
'head'agent's compile without change rows, with no helper and no folding rule for a host's own rows; the fail-safe leavingcontext_lengthandauth400s to the error policy; the split of the changelog; and a billed run on Anthropic's API in place of calling'entries'experimental. At her call,22dfd5a59btakes out comments this pull request had added, keeping docs on the package's exports,Agent's public members andsrc/types/, commentsmainalready had, and short notes on what context-manager, membrane and chronicle do. Effie-2756 made the changes. Our internal review read them, and then the fixes for what it found. Among its findings were the order on the head, the record cleared before the rows, and the one case where the narrowed fail-safe gives up a resend that could fit. Phoebe-2419 read the last of those fixes and the cut of comments. Nigel-2405 ran the check on Anthropic's API.For the merges of
4429afe2b6anda25e38b1bf, Effie-2756 made the resolutions and Ada-1017 decided how the binding's doc and the views take #938 and #975. Desmond-2579 read both merges, and the test for suppressed rows. For the findings of both automatic reviews, Ada-1017 decided how each is answered, Effie-2756 made the changes, and Crispin-2713 read them. For the review of48ede2f824and the notes of the approval atc045b9870b, Ada-1017 decided how each is answered, Effie-2756 made the changes, and Crispin-2713 read them. For the ask in the approval by @slimepriestess atd44fc03d29, a log line for a change row the store can't write, Oscar-1808 decided it's taken, Effie-2756 made the change, and Crispin-2713 read it. For the findings of the second review by @Anarchid, at5e4a561cc3, Ada-1017 decided how each is answered, with Oscar-1808 ruling that R2-2's change is filed as breaking, Effie-2756 made the changes, and Crispin-2713 read them. For the review of2289aef75athat asked for changes, Ada-1017 decided how each point is answered, Effie-2756 made the changes and ran the summary-merge runs, and Desmond-2579 read them.Fiona-2757 wrote this description, from Effie-2756's drafts and notes, the fragment's line that
a91ad6d5d2adds, and the changelog fragments' edits in36da4a5a2c. Effie-2756 updated it for the merges since22dfd5a59b, both automatic reviews, the second review by @Anarchid and the reviews of2289aef75a,48ede2f824andc045b9870b, and the approval atd44fc03d29.