Skip to content

feat(agent): declare later tool and system-text changes in the context - #690

Open
nissa-seru wants to merge 128 commits into
anima-research:mainfrom
nissa-seru:feat/tool-declarations
Open

nissa-seru wants to merge 128 commits into
anima-research:mainfrom
nissa-seru:feat/tool-declarations

Conversation

@nissa-seru

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

Copy link
Copy Markdown
Contributor

Why. A request's tools and system text come first in its cached prefix, so today any change to an agent's tools, such as a server connecting, rewrites the whole prompt cache from the first byte. On a model that binds its thinking to everything before it, such a change also sets aside every thinking block the context holds.

This adds an opt-in setting, toolDeclarations: 'entries', under which a later change to an agent's tools, or an addition to its system text, goes into its context as a system change where it took effect, so the provider reads its prompt cache through the change. 'head' stays the default.

For a reviewer, 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:3035 at this head, and the fallback in assembleActivationRequest, :2250). A strategy that keeps change rows where this pull request doesn't expect them would change both. We ran context-manager 0.24.1's PassthroughStrategy, AutobiographicalStrategy and KnowledgeStrategy with a stand-in for membrane, at a context budget of 6,000 tokens, at 740f4f979d, an earlier commit of this pull request ("Under context-manager's strategies", below). Those runs had no real provider or summarizer, and no real sizes.

A change to an agent's tools, or an addition to its system text, on an agent set to 'entries'

  • base (main): there's no such setting: every request carries the agent's current tools and system text first, so the change rewrites the prompt cache from the first byte.
  • head: the agent keeps a recorded head, the tools and system text of its first request or of the head's latest move. The change is stored in its context as a system change where it took effect, and membrane sends it as a mid-conversation system message. It's for models that take one through membrane's native formatter, with an adapter that declares sendsSystemChanges.

What a host that sets 'entries' needs to know:

  • A host that wraps membrane's adapter forwards sendsSystemChanges, or, by membrane's documentation, a request carrying a change is refused before sending.
  • After a request carrying a change is refused, the agent keeps to its head, and an operator line (head-resend) says so, with the remedy where membrane recognised the refusal. The exception is a refusal membrane files as context overflow or an account refusal and doesn't recognise as a refusal of the change, which goes to the error policy.
  • It saves cache 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.
  • Under context-manager's autobiographical strategy, in 0.24.2 as in 0.27.0, the summaries on either side of an agent's own change row don't merge with each other while the row stays in place, which can make requests longer. In our runs of it at an earlier commit, adding a tool every 5 or 10 turns with a stand-in for membrane, an 'entries' request reached 38,351 characters where the same turn on 'head' had 26,072, and no provider priced either.
  • A host that stores change rows of its own, without an agent's change metadata, removes them itself: the agent neither folds them onto its head, consolidates them nor removes them.
  • Where the host knows whether a model binds its thinking, it sets thinkingBinding.binds. Only with true does the framework leave out thinking a request would carry under a prefix it wasn't signed under. With binds left out, the request carries that thinking: with thinkingBinding.dropBlock the provider drops it, and without it an account that enforces the binding refuses the request, and one that doesn't decides for itself. That happens:
    • after a refusal keeps the agent to its head, until the replies signed over the change leave its window or it's built again;
    • on a direct call whose caller passes tools or system text other than what the agent's recorded head and own change rows declare: the thinking signed after the first change row the call leaves out, and all of it where the heads differ;
    • after a forced move of the head, until the replies signed before it leave its window.

A host's own change rows, for an agent on 'head', the default

  • base (main): requests carry them as stored. They reach the model where membrane's formatter and adapter both handle system changes, as its native formatter and Anthropic adapter do; with any other pair, membrane refuses the request before sending it.
  • head: an agent on 'head' leaves them out of its requests, and out of its compile where the framework opens its context manager. This is breaking: the host sets 'entries' for the agents that should send them. No setting sends them to the persistent subconscious, which on main carried them, and the direct API sends them only under 'entries', as the agent's requests do.

PresentationRetractReason, for every host

  • base (main): a presentation:retracted trace gives one of ten reasons, from 'hide' to 'unsent-injection'.
  • head: it adds 'head-moved'. A move of an agent's head that takes the agent's own change rows out of its record retracts them, with this reason, from each other agent's window that held them. This is breaking for a host that handles every reason, as an exhaustive switch does.

TraceEvent, for a host that handles every type

  • base (main): a trace event's type is one of those main lists.
  • head: it adds head:declared, head:moved and head:resend. This is breaking for a host that handles every type, as a Record<TraceEvent['type'], …> or an exhaustive switch does.

ModuleContext.viewKeeps, for a host's own change rows

  • base (main): it answers by a row's metadata, so it reads a host's change row in an agent's window as kept.
  • head: it answers whether the agent's request carries the row, so it reads such a row as not kept wherever the request leaves it out, an agent on its head included. This is breaking for a module that relied on the old answer.

An ephemeral agent's selfParticipants from its spec

  • base (main): a spec's selfParticipants reach the agent's record but not the live agent when its config names none, so a live shared taskForkSpec child reads its parent's turns as another's, while a resume reads them as its own.
  • head: the live agent takes them, as AgentSpec.selfParticipants' doc says. The changelog files this as fixed.

A request's tools, on either setting

  • base (main): a request sends the tools it's given, and kv-unified hashes them as given, so a field whose value is undefined, which the wire leaves out, still counts in the hash.
  • head: a request the framework builds for a turn takes a copy of the head's tools as JSON writes them, so a value JSON can't write, such as a BigInt, fails as the request is built rather than as it's sent. The direct API passes the tools it's given as they are, except where it goes on an 'entries' agent's recorded head, whose copy it sends. kv-unified hashes the tools as the wire carries them.

What else changes for an agent on 'head':

  • A change row is never a message said to the agent, and none goes into its live turn. /undo N and the poisoned-history shed pass over change rows, and history's tools show a change as [system change].
  • A change row after a batched wake's message no longer makes the wake write its line, since the check of what's newest passes over change rows as it does clock rows. The check still reads only the 32 newest rows, as on main, so a change row that pushes the wake's message out of them still has the wake write its line.
  • A rollback refused because rows one write presented with its target follow it counts and names only the messages among them, never a change row, and a surgery preview counts a change row no live agent's request carries as never presented.
  • A resident's registry record holds its selfParticipants and presentAs.

Documented rule: AgentConfig.thinkingBinding.binds in src/types/agent.ts

  • base (main): "Whether this agent's model binds its signed thinking to the conversation before it, as the host knows from its own model records. … Set: …" and "Default false: compiles and streams are as they were."
  • head: "Whether this agent's model binds its signed thinking to the conversation before it, so that a change to anything before a thinking block sets it aside, as the host knows from its own model records. One fact with three states, each feature taking its own safe side where it's left out". true turns on what main's "Set" did, and of the "digests of what the request carries ahead of its messages" it adds "Under toolDeclarations: 'entries' these are the recorded head's, which the request carries". false: "Compiles and streams are as they were, and nothing is held back to keep thinking." And "Left out: not declared. What relies on the declaration stays off, as with false, and a choice that could set kept thinking aside is made as for a model that binds (mayBindThinking, agent.ts), as with true."

Documented rule: AgentConfig.selfParticipants in src/types/agent.ts

  • base (main): "…so the parent's turns read as its own history. Model-facing role assignment only, applied at request assembly — stored authorship is never rewritten."
  • head: "…so the parent's turns read as its own history. Stored authorship is never rewritten. A system change stored with one of these names' change metadata is this agent's own too: its view keeps it, and a move of its head takes it out of the agent's own context, as does a request on the head that clears a recorded head or whose compile holds one of the agent's own change rows."

Documented rule: AgentSpec.selfParticipants in src/dendrite/types.ts

  • base (main): "Stored participants that render as this agent's own (assistant) turns at request assembly. Stored authorship is never rewritten; this is a model-facing role assignment only."
  • head: "Stored participants that render as this agent's own (assistant) turns at request assembly, and whose system changes are its own (see AgentConfig.selfParticipants). Stored authorship is never rewritten."

Documented rule: why rows leave an agent's window, at presentation:retracted in src/types/trace.ts

  • base (main): "…a suppression a start finished, resume, and a refusal rewind's shed from a window that is still the record, rewind), or that no live context heard, found at boot (boot)."
  • head: "…a suppression a start finished, resume, a refusal rewind's shed from a window that is still the record, rewind, and an agent's own system changes a move of its head took out, head-moved, the head:moved trace after it saying why, or none where the head couldn't be stored and none was recorded), or that no live context heard, found at boot (boot)."

Documented rule: FrameworkConfig.exhaustedTurnRearmMs in src/types/framework.ts

  • base (main): "…A completed turn starts the count again, and so does a message said to the agent: not a system message or notice; not an edit, reaction or deletion, …"
  • head: "…A completed turn starts the count again, and so does a message said to the agent: not a system message, notice or system change; not an edit, reaction or deletion, …"

Documented rule: StartStreamResult.continuePrompt in src/agent.ts

  • base (main): "…else, when it ends on tool results, or on tool results and then stored context injections alone, the prompt of the reply that called those tools (toolLoopPrompt); else none."
  • head: "…else, when it ends on tool results, or on tool results and then stored context injections and system changes alone, the prompt of the reply that called those tools (toolLoopPrompt); else none."

Documented rule: PersistentReaderConfig.model in src/tune-out/tools.ts

  • base (main): "…Another id, compared exactly, makes the subconscious leave out the fields that describe the primary's model, its window and its thinking binding (SUBCONSCIOUS_MODEL_FIELDS); physicalWindowTokens and thinkingBinding set its own."
  • head: "…Another id, compared exactly, makes the subconscious leave out the fields that describe the primary's model: its window, its thinking binding and its tool declarations (SUBCONSCIOUS_MODEL_FIELDS). It then declares its tools and system on its head, and physicalWindowTokens and thinkingBinding set its own window and binding."

Documented rule: the history module's extract tool description, in src/modules/history/index.ts

  • base (main): "…format:"raw" returns the content blocks unmodified. …"
  • head: "…format:"raw" returns the content blocks unmodified, except that a system change comes back as its type alone (what it declared may be another agent's tools and system text, so it is withheld). …"

Documented rule: ModuleContext.viewKeeps in src/types/module.ts

  • base (main): "…a window written down from the record the residents share can hold rows the view never sends: another agent's stored context injection, and, for an agent whose view leaves held traffic out (a resident), a row a tune-out holds from the residents."
  • head: "…a window written down from the record the residents share can hold rows the view never sends: another agent's stored context injection or system change, every system change for an agent on its head, and, for an agent whose view leaves held traffic out (a resident), a row a tune-out holds from the residents."

Documented rule: request-only rows, in src/silent-heartbeat.ts's description

  • base (main): "…A reply follows the prompt when what it was sent ends on that [Continue], or on the tool results of a reply that followed it (toolLoopPrompt), stored context injections after those results aside: a stream start and each round of a stream alike."
  • head: cut, with the sentence after it ("A reply sent anything else after the results (a message that arrived meanwhile) follows that, and carries no prompt."). With this change, system changes after those results are aside too, so the sentence no longer held.

Documented rule: a turn's pinned inputs, in README.md

  • base (main): "…starting at the first such row that holds more than tool results, and removes the pin when the turn ends."
  • head: "…starting at the first such row that holds more than tool results and isn't a system change, and removes the pin when the turn ends."

Documented rule: a server's dynamic tools under hints, in README.md

  • base (main): "…never enter it, since every change to the list rebuilds the prompt cache."
  • head: "…never enter it, since on 'head', the default, every change to the list rebuilds the prompt cache."

Documented rule: /undo n's count, in README.md

  • base (main): "…/undo n counts back over the rows of the agent's own slot in its window. …"
  • head: "…/undo n counts back over the rows of the agent's own slot in its window, every row but system change rows: the change rows among the last n go with them uncounted. …"

Documented rule: a rollback refused for the rows one write presented with its target, in README.md

  • base (main): "…takes them out. A row holding a result of a call at or before the target …"
  • head: "…takes them out. The refusal counts and names the messages among them and never a system change row, which it doesn't advise removing; a target followed in its write by change rows alone is refused saying so. A row holding a result of a call at or before the target …"

Documented rule: a surgery preview's neverPresented, in README.md

  • base (main): "…no window holds them, or only windows whose view leaves them out (a row a tune-out holds); …"
  • head: "…no window holds them, or only windows whose view or request leaves them out (a row a tune-out holds, or a system change row for an agent on its head); …"

Documented rule: the exported context shapes' view of an ended agent, in src/context-options.ts

  • base (main): "…The framework opens its live agents with these, and opens an ended one's context again with the same view (reopenAgentContext). …"
  • head: "…The framework opens its live agents with these, and opens an ended one's context again with the same view (reopenAgentContext), except that an inspection's reopen keeps a host's change rows and the agent's own, which a compile on 'head' leaves out. …"

Documented rule: what a resident's view leaves out, in src/context-options.ts

  • base (main): "…their context, which they share the slot with it but never its conversation. Both views also omit legacy reasoning-loss notices."
  • head: "…their context, which they share the slot with it but never its conversation, and the system change rows its compile leaves out: on 'head' every one, and under 'entries' another agent's (pass its toolDeclarations and selfParticipants). Both views also omit legacy reasoning-loss notices."

Documented rule: what the subconscious's view leaves out, in src/context-options.ts

  • base (main): "…Its view leaves out only the residents' stored context injections and what a suppress rule hid, in this shared view, along with legacy notices. …"
  • head: "…Its view leaves out only the residents' stored context injections, what a suppress rule hid and every system change row not its own, in this shared view, along with legacy notices. …"

@antra-tess, this changes the eighteen documented rules quoted above.

What hasn't run:

  • Apart from one run on Anthropic's API that declared one change, with its replies from a stand-in, no test or run sends a request to a model. No request to a provider moved the head, consolidated or met the fail-safe.
  • The runs under context-manager's strategies were at earlier commits, with a stand-in for membrane and no real sizes. Apart from those runs of the autobiographical strategy above, each made one change. None ran a change that removes, redefines or reorders tools, the residents' shared slot, a fork, or WindowedPassthroughStrategy, which the subconscious uses.
  • Our runs were all on Linux. No test or run has a resend that fails at a deadline or under a provider hold, a conversation fork's compile refused over budget, a fork created by routing a channel's message at the head resend, what a fork or derived child copies from a 'head' source's change rows, or a host's change rows in an 'entries' agent kept to its head.
Credits

Effie-2756 wrote the code, and Ada-1017 decided its shape and how most review findings are answered, with Oscar-1808 deciding the ask in slimepriestess's approval and how one finding is filed. Oscar-1005 shaped how it meets membrane and context-manager. Ezra-2423, Gladys-2413, Desmond-2579, Phoebe-2419, Crispin-2713 and William-2616 read its rounds and merges, and Nigel-2405 ran the check on Anthropic's API. Fiona-2757 and Effie-2756 wrote the description, and Ramona-2416 put it in this shape.

Details for agents

Head cbbd862adb, into main. This head last merged main at 36b6dfe9de, where src/ has no toolDeclarations.

This adds toolDeclarations to 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.

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 tools or system. A change sent that way writes only its own bytes. membrane documents (SystemChangeContent in src/types/content.ts, and sendsSystemChanges in src/types/provider.ts, at v0.8.0) that it sends a message holding only system_change blocks that way through its native formatter and an adapter that declares sendsSystemChanges. 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 declare sendsSystemChanges, 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 carry systemChangeRefused: true, except that a formatter that can't carry the request's tools at all refuses first, without the flag.

Depends on

  • membrane ^0.8.0 || ^0.9.0 || ^0.10.0, main's range since build(deps): take context-manager ^0.27.0 and admit membrane 0.10 #1006, with system changes since 0.8.0 (SystemChangeContent, isSystemChangeContent, sendsSystemChanges, systemChangeRefused).
  • context-manager ^0.27.0, main's floor since build(deps): take context-manager ^0.27.0 and admit membrane 0.10 #1006. 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, finds CompileResult.firstChanged by comparing content, and refuses a change written between a tool call and its result. With thinkingBinding.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.
    • At cbbd862adb, this head, on Linux: context-options, tool-declarations, tool-declarations-framework and tool-declarations-failsafe pass whole, 188 tests. This pull request's CI passed all 16 checks. It differs from bca9f4d181 by one doc line cut from src/stable-hash.ts.
    • At bca9f4d181, on Linux: context-options, tool-declarations, tool-declarations-framework and tool-declarations-failsafe pass whole, 188 tests. This pull request's CI passed all 16 checks. It differs from 791e3e26a3 by two test-only commits: two comments cut from test/context-options.test.ts, and a self participant's leg in its parity test.
    • At 791e3e26a3, on Linux: context-options, tool-declarations, tool-declarations-framework and tool-declarations-failsafe pass whole, 188 tests. This pull request's CI passed all 16 checks. It differs from 98088534f0 by the round answering Anarchid's nightly review R3: the exported context shapes, the cost figures for Fable 5.1, and two doc sentences.
    • At 98088534f0, on Linux: tool-declarations, tool-declarations-framework and tool-declarations-failsafe pass whole, 182 tests. This pull request's CI passed all 16 checks. It differs from 0f2e7b9302 by one test, of a host's change row through the direct API where the caller's tools differ from the recorded head.
    • At 0f2e7b9302, on Linux: tool-declarations, tool-declarations-framework and tool-declarations-failsafe pass whole, 181 tests. This pull request's CI passed all 16 checks. It differs from 0854ab7e3c by the carrying-cost sentences in toolDeclarations' doc and the README, which now say what carrying adds over 'head'.
    • At 0854ab7e3c, on Linux: tool-declarations, tool-declarations-framework, tool-declarations-failsafe and system-changes pass whole, 200 tests. This pull request's CI passed all 16 checks. It differs from 8ca889bfcc by a sentence in toolDeclarations' doc and the README on what carrying changes costs, and by the difference comparing tools with stable-hash's encoder.
    • At 8ca889bfcc, on Linux: tool-declarations, tool-declarations-framework and tool-declarations-failsafe pass whole, 180 tests. This pull request's CI passed all 16 checks. It differs from b258e51975 by the operator line for a refusal membrane recognises and six Agent methods marked internal. Before it was committed, its alert, taken out, failed only the tests of that line (Tests, below).
    • At b258e51975, on Linux: tool-declarations, tool-declarations-framework, tool-declarations-failsafe, dendrite-fork-view, rollback-cut-accounting, continuing-start-silence and turn-inputs-pin pass whole, 272 tests. This pull request's CI passed all 16 checks. Each of this round's fixes, taken out alone where it was built, failed only its own tests (Tests, below).
    • At 1973fd50ea, on Linux: tool-declarations-framework and tool-declarations-failsafe pass whole, 99 tests. With 902a0e8eba's src/framework.ts in its place, they fail exactly three: the two operator-line tests for a forced share and the note arm. This pull request's CI passed all 16 checks.
    • At 902a0e8eba, which differs from 8513aea411 only by one test's name: this pull request's CI passed all 16 checks. With a probe file added, on Linux, a resident's module added a tool whose definition passed a fifth of the request, against a stand-in that refuses thinking signed under another prefix, as an account that enforces the binding does without the drop control. With binds left out, the two requests after the forced move carried the earlier replies' thinking and were refused. With binds: true, the first carried no thinking and the second only thinking signed under the new head, and both were answered.
    • At 8513aea411, on Linux: 27 test files, those of the earlier runs and turn-inputs-pin, pass whole, 542 tests. This pull request's CI passed all 16 checks. Before it was committed, nine of its pieces were each taken out alone, and each failed its own tests and no other test that passed with nothing taken out (Tests, below).
    • At 7a1305b98c, which differs from 85898ac539 only by the added changelog fragment's sentences on a move: this pull request's CI passed all 16 checks.
    • At 85898ac539, which differs from 5c8a8cb9b4 only by putting a shared child's seed-head read back where it was, on Linux: 26 test files pass whole, 510 tests, and with either of the two head reads for finding 19 of the automatic review of 26d41c6192 put back before its copy, each fails only its own test.
    • At 5c8a8cb9b4, which differs from 46ce6343e3 only by one line of the breaking entry: this pull request's CI passed all 16 checks.
    • At 46ce6343e3, on Linux: 26 test files, those with the tests for the review of 26d41c6192 and those of the paths it touches, pass whole, 510 tests, and with the fix for that review's finding 21 taken out alone, only its two tests fail. Before the last changes made answering that review, which cut comments, moved one helper and made moveHead one path, each of eleven fixes taken out alone failed only its own tests (Tests, below).
    • At 26d41c6192, on Linux: the 42 test files the next two bullets describe and foreign-rounds pass whole, 1,252 tests. It differs from b577bf2e72 only by consolidation's count ("Consolidation", above). This pull request's CI passed all 16 checks.
    • At b577bf2e72: this pull request's CI passed all 16 checks. On Linux, the 42 test files the next bullet describes and foreign-rounds, whose "is its name alone" is main's test of the subconscious's own voice, pass whole, 1,252 tests. It differs from e33af3b595 only by the drop "With main" names.
    • At e33af3b595, on GitHub's ubuntu-26.04 runner (image 20261101.186.1) with Node 22.12.0, installed as CI does with no lockfile, which printed membrane 0.10.2, context-manager 0.27.0 and chronicle 0.6.0: the build succeeds, CI's scripts/test-as-ci.mjs runs 11,082 tests, with 11,019 passing, none failing, 62 skipped and 1 todo, and CI's three unforced files (stop-with-work-in-flight, stop-with-host-operations, conversation-routing) pass whole, 133 tests. On Linux, the 42 test files covering what this head's merge of 36b6dfe9de and its ports change pass whole, 1,241 tests.
    • At dc8800379b, 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, and system-changes (17 tests), tool-declarations (69), tool-declarations-framework (70), tool-declarations-failsafe (16), dendrite-fork-view (15), dendrite-derive (17) and dendrite-lifecycle (32) pass whole.
    • At 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, and system-changes (17 tests), tool-declarations (69), tool-declarations-framework (70), tool-declarations-failsafe (16), dendrite-fork-view (15), dendrite-derive (17) and dendrite-lifecycle (32) pass whole.
    • At 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, and system-changes (16 tests), tool-declarations (67), tool-declarations-framework (70), tool-declarations-failsafe (16), dendrite-fork-view (15), dendrite-derive (17) and dendrite-lifecycle (32) pass whole.
    • At 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, and system-changes (16 tests), tool-declarations (67), tool-declarations-framework (70), tool-declarations-failsafe (16), dendrite-fork-view (12), dendrite-derive (17) and dendrite-lifecycle (32) pass whole.
    • At 48ede2f824, this head before the commits answering the review of 48ede2f824, 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, and system-changes (16 tests), tool-declarations (64), tool-declarations-framework (68), tool-declarations-failsafe (16), dendrite-fork-view (10), dendrite-derive (17) and dendrite-lifecycle (32) pass whole.
    • At 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, and system-changes (16 tests), tool-declarations (60), tool-declarations-framework (65), tool-declarations-failsafe (14), dendrite-fork-view (10), dendrite-derive (17) and dendrite-lifecycle (32) pass whole.
    • At 76e2d43e48, this head before the commits answering the automatic review of 76e2d43e48, 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's locked-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) and present-while-acting (94) pass whole.
    • At 7919f77730, this head's merge of 4429afe2b6 and its test for suppressed rows, before the commits answering the automatic review of 7919f77730, 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 same package.json, which printed membrane 0.8.0 and context-manager 0.24.2: the build succeeds, and tool-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) and event-gate (64) pass whole. With observerView'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-e2e and tune-out-gate-composition run again, and only dendrite-fork-view's new suppressed-row test fails.
    • At bc1ca99be2, this head before its merge of a25e38b1bf, in a clean copy on the same platform, installed as the run at 7919f77730 was: the build succeeds, and batched-wake-line (33 tests), turn-inputs-pin (17), mid-turn-injection-as-compiled (23), exhausted-turn-rearm (86), dendrite-fork-view (9) and tool-declarations-failsafe (14), the files its last commits change, pass whole. With 7919f77730's src/framework.ts, exactly two assertions fail: the new test's, on a deferred system change in batched-wake-line, and the changed one on the operator line's wording with binds: true in tool-declarations-failsafe.
    • At 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, and tool-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) and dendrite-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 at 2b0c223934 and at 22dfd5a59b.
    • At a91ad6d5d2, this head before its merge of 48f26ce56c, 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, and tool-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) and system-changes (15) passed whole.
    • At 67ba70e53d, an earlier commit of this head, in a clean copy on the same platform, installed the same way: the build succeeded, and exhausted-turn-rearm, prose-drafts, subconscious-model-fields, thinking-binding-binds, unsent-compiles, tool-declarations, tool-declarations-framework, system-changes, dendrite-fork-view and dendrite-lifecycle passed 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.

Changes

Line numbers here and below are this head's unless they name another commit.

  • The setting: AgentConfig.toolDeclarations, 'head' or 'entries', default 'head' (src/types/agent.ts:235). null is read as unset, and anything else is refused, naming the value's kind, never the value (assertOneOfSetting, src/config-checks.ts:43). It isn't a runtime setting, since turning it on is the host's choice from what it knows of the model.
  • The pure part (src/system-changes.ts): folding a context's changes onto a head (effectiveDeclaration, src/system-changes.ts:204), and the difference from what the agent advertises now (declarationDifference, :218).
    • Tools are compared by name and canonical definition, so order and key order don't count.
    • System text is compared by blocks: the agent's prompt, then each system injection's text, counted as a multiset. New blocks are an addition. Anything else, a block changed, gone or cut, is a replacement.
  • The request (Agent.settleHead, src/agent.ts:2910), after the committing compile. It reads only the agent's own change rows (ownSystemChanges, src/system-changes.ts:161): a change compiled from stored rows whose change metadata, kind: 'system-change' with an agent, names one of the agent's own participants, its name or one of its selfParticipants, 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:651). 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.
    • with no recorded head, the current one is recorded, an adoption, which sends what the request would have sent anyway;
    • a replacement moves the head to what's current (moveHead, src/agent.ts:3001);
    • otherwise the difference, less what the rest of the request already carries the same way (a tool defined the same, a tool already removed, a system block another row already adds), is stored as one change at the end of the context, and the compile runs again (withoutCarried, src/system-changes.ts:251). 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:2944–2952). A write that throws though the store holds its row (WriteLandedError, or an outcome the store reads as stored) is declared all the same, and the compile runs again with it (storedOfThrownWrite, :2947), since the row is in the window the next compile reads. 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's firstChanged reads the change as sent, not as rewritten (src/agent.ts:2962). 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:34), 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:61) 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:2498). In the residents' shared slot the call can be another resident's;
    • moving the head records a copy of it (copyHead, src/system-changes.ts:174), 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 once the record is stored, or, with none recorded before, whether or not it's stored (src/agent.ts:3015). A head that can't be stored moves nothing while a head is recorded: the request goes on the recorded head with its change rows. With none recorded, the request goes on what's current: the agent takes its own change rows out of its context and compiles again, on the one path a successful move takes (:3015), only without the trace, so the compile context-manager keeps, its firstChanged baseline and with binds its thinking-binding record, is the request sent. At 26d41c6192 that path sent a filtered copy of a compile already committed and kept the rows stored, so that record held rows the request never carried. It also compiled again only when it removed rows. It now compiles again on the success path's condition, which also covers a compile made for another head; by reading, the two conditions give the same compile in every case we found. A removal the store refuses now fails the turn. A later request moves the head again;
    • kv-unified hashes the head that goes out, and with thinkingBinding.binds: true each compile is told digests of that head (bindingCompileOptions, src/agent.ts:2459). 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, with the same encoder (wireJson, src/system-changes.ts:200, on canonicalJson, src/stable-hash.ts:3), 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;
    • a dry run (previewActivation) shows the same, compiling again (dry) for another head, and stores nothing. A dry compile has no firstChanged, 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. A preview's appended rows count as carried, so it declares no tool a host's appended change already defines, as a real compile with that row stored doesn't (src/agent.ts:2978).
  • On the head, change rows stay out of the request, and out of the compile too: every context manager the framework opens to run an agent configured 'head', other than the subconscious's, leaves them out of its view (compiledView, src/framework.ts:3474). That covers a resident's (:19017), an ephemeral agent's, isolated (:12184) or derived (:12862), a resumed job's (:12669) and a conversation fork's (:21066). So a host's change rows take none of such an agent's budget. The subconscious's view leaves out every change row not its own (observerView), where on main its merged view carried a host's change rows from the residents' slot; the breaking entry says so. A derived child always gets a filter of its own (:12862). An inspection (inspectAgentContext) opens one with the agent's view and without the 'head' filter (:12648, :12669), so it shows the change rows that view keeps. The filter goes by the agent's configured toolDeclarations, which a manager takes when it opens (:3474–3480). 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:2925). 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, with binds: true, context-manager's thinking-binding record. On its head, the direct API (runInference) sends its caller's tools on the current head and leaves every change row out (src/agent.ts:2060–2061).
  • A request's last message is read past the rows after it that aren't turns, change rows and main's stored context injections, in either order (endsOnOwnMessage, src/agent.ts:181), 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.
  • Consolidation (Agent.consolidation, src/agent.ts:3035): once the agent's own changes reach a tenth of the request, the head moves to what's current at the first committing compile whose firstChanged 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. The share is in characters of JSON on both sides, from the one request the compile builds (requestCharacters, src/system-changes.ts:178): the changes' blocks, and the request's head, its tools and system text, with its messages as the request sends them (rowsAsSent, src/agent.ts:701): another agent's rounds as this agent is sent them, without the blocks the sanitize pass leaves out or the pricing stamp, under the agent's own name, and with a preview's appended rows. Both sides go through one count (pricedCharacters, src/system-changes.ts:182), and the request itself is built from the same rows. The request's count leaves out what a provider doesn't price by its characters: a base64 source's data, nested ones included, a generated image's data, a redacted thinking block's data, a thinking block's signature, and a block's provider-native rawItem. It also leaves out what a request on the native formatter doesn't send: a parsed block's rawXml and rawXmlImagesAt, a call's unparseableInput, and a block's tokenEstimate. Those fields are left out of any object whose type is a string, as every content block's is, so a tool parameter of one of those names counts, while a tool call's input or a tool's schema that holds one beside a string type counts without it, though it's sent. JSON runs denser than prose by context-manager's own estimators (2.3 against 2.9 characters a token), so tool changes read as a smaller share than they would in tokens, and a move comes somewhat later than the tenth and the fifth intend. Nothing corrects for that. The count runs only when the agent has a change of its own. At 26d41c6192, before the count read the rows as sent, on Linux with Node 24.22.2, it took a median of 2.44 ms for a request of 1.2 million characters, and 11.14 ms for one of 4.66 million. 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, and firstChanged falls after it. firstChanged is 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:415) says whether the model binds its thinking, in three states read through two predicates (src/agent.ts:222, :226). A choice that could set kept thinking aside, which consolidation is, counts the model as binding unless binds is false (mayBindThinking). What relies on the declaration, the context manager's record, takes true alone (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 from firstChanged on is already set aside by the compile's own change, so a model that may bind consolidates for free only where no thinking sits before firstChanged in the rows as sent, so a foreign round's thinking that transcript mode doesn't send doesn't hold a free move back.
    • When no free compile comes: under the strategies as built it may never come for a binding model (the autobiographical head window keeps its first replies' thinking), nor under passthrough when its window passes the first change before the changes reach a tenth, since that compile is its only one from the first change. So once the changes a committing compile holds reach a fifth of its request, the head moves at that compile anyway (forced: 'share'). A change the agent declares counts from the compile after the one that declares it, so the request that first carries a change can pass a fifth, and the head moves at the next one; that costs writing the change itself once, which a move at once would save. 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, whose cache reads cost a twentieth of their input, and 490 to 790 on Fable 5.1, whose cache reads cost a fortieth (membrane 0.10.3's pricing table, dist/registry/default-pricing.js:39–40, :59–60, :67–68). toolDeclarations' doc gives these counts for what carrying adds over 'head': a replaced or removed tool's old definition, which the head keeps, and the rows' wrapping. A free consolidation writes the head and any messages before firstChanged beyond what its compile writes anyway, never more than a forced move, so it repays no later. 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 245 to 395 requests. The model counts every change's tokens as saved, though an added tool's definition, or added system text, moves into the head rather than going away. It doesn't count a replaced or removed tool's old definition, which the head keeps and every request sends until a move, and which the share doesn't see either. Nor does it price what a forced move costs a binding model's thinking. When a request's first committing compile is refused with OverBudgetError, or with UncoveredDropError for 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, before settleHead (src/agent.ts:2250), in every state (takeChangesOverBudget, :2982). 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:645, read at :2986). 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 a compiledView manager 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, inside settleHead (:2963), which takes the change into the head. A forced move costs one whole write, and, with thinkingBinding.binds: true, the thinking the request held. With binds left out, the requests after it carry that thinking under a prefix it wasn't signed under: with thinkingBinding.dropBlock the provider drops it, and without it an account that enforces the binding refuses them. It raises an operator line (head-consolidated), which names that thinking outright with binds: true, as a condition with binds left out, and not at all with binds: 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:3011). The move forced by the budget is never made in a dry run (:2251).
  • The fail-safe (keepToHeadAfterRefusal, src/framework.ts:18925, called at the stream's error event, :30878): 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 as context_length or auth (:18928), 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:7533), so the resend's path doesn't ask (:30887; test/tool-declarations-failsafe.test.ts:209). 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:268). That leaves to the error policy one case where a resend on the head could fit: a context_length 400 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:20159). The agent keeps to its head until a new Agent is built for it, as at a restart. Its compile, opened under the 'entries' view, still holds a host's change rows that its requests on the head leave out. So with thinkingBinding.binds: true, the fail-safe sets aside the thinking of each of its replies after the first change row the refused stream sent, in its compile or handed at a boundary. It notes them as dropped on that stream's compile (context-manager's noteThinkingDropped, src/framework.ts:18943), and from the next compile on they go without it. Without that, each later request sent thinking signed under a prefix that held such a row until the agent was built again, and without thinkingBinding.dropBlock an account that enforces the binding refused it. An agent whose model binds but whose host leaves binds out still meets that, since what relies on the declaration stays off without it, as binds' doc says (src/types/agent.ts:394). After such a refusal, its requests can carry that thinking until those replies leave its window or it's built again. With thinkingBinding.dropBlock, the provider drops it; without it, an account that enforces the binding refuses those requests, and one that doesn't decides for itself. head:resend counts the replies noted (thinkingSetAside), and the log line gives their number. If the note doesn't land, the log line says that, where the agent's head didn't move with the refusal, its requests can carry that thinking until those replies leave its window or the agent is built again, and that without thinkingBinding.dropBlock an account that enforces the binding refuses them. With binds: true, what stays is loss, not refusal: the compile on the head still counts a host's change rows against its budget, a host's change presented at a later boundary reaches the compile and no request, so the thinking after it is set aside, and after a rebuild the agent's first request may be refused once, which the fail-safe then recovers. The refusal never reaches the error policy. The default policy, DefaultErrorPolicy, treats a 400 as terminal on the first failure (src/framework.ts:2946). 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 (attempt 1, counting from 0) (test/tool-declarations-failsafe.test.ts:217). The fail-safe decides before that rule (src/framework.ts:30897), 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. The fail-safe raises an operator line (head-resend) at each resend (src/framework.ts:18936), so once in an agent's build: the agent then keeps to its head and sends no change for a provider to refuse. For a refusal membrane recognises, the line names the remedy, toolDeclarations: 'head' for a model that refuses system changes, or a formatter and an adapter that send them; for a 400 membrane doesn't recognise as a refused system change, it says the 400 may not be about the change. The line quotes the error, says what the log line says of the thinking, or, where no reply was named, the thinking the resend can set aside, as binds says, and says that the agent keeps to its head until a new Agent is built for it, as at a restart. Where the note lands, its log line and head:resend say what was set aside too.
  • The mapping an agent runs with: an agent createEphemeralAgent makes with a spec that names selfParticipants, and a config that names none, takes the spec's, as AgentSpec.selfParticipants' doc says (src/framework.ts:12119), so the agent, its record and a resume agree. A config that names an empty list stands as given, at creation as at a resume, and the record then holds no list (:12218, :12225), since the registry records none for an empty one (src/dendrite/registry.ts:288). On main the record stated them and the live agent didn't take them, so a live shared task fork read its parent's turns as another's. A conversation fork's record carries the template's selfParticipants and presentAs it runs with (src/framework.ts:21182).
  • Where heads are kept: each agent has a store state, 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:18881); 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 and named in the log. Under 'entries' its agent adopts again, and an agent on its head sends no request on it. One that can't be read sends that agent its current head, and the log says so once a branch (src/framework.ts:18896). 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:18902). 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.write and .delete now return a boolean (src/system-changes.ts:12–13). The framework's store returns from write whether the head is stored, and from delete whether no record is left (src/framework.ts:18863, :18902). 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, an ephemeral agent and a resumed one keep their heads in memory, per branch and name, the branch read from the agent's own context manager (memoryHeadDeclarations, src/system-changes.ts:16). An idle live agent's context follows a branch switch, which refuses only agents that are busy or have a turn alive (src/framework.ts:17041), so a head kept by name alone would be read on a branch it wasn't recorded on. A branch it holds no head for reads none, and the agent adopts there; the framework's store instead carries a recorded head into a branch made from another, as chronicle's createBranch inherits states. A head that can't be stored with none recorded, or with a record that can't be read, sends the current head without the agent's own change rows, and the log says so (src/framework.ts:18908, :18896); a record that reads again before a write lands is the head the next request declares from, which then declares the difference or replaces it. A record set aside as damaged is written over when the agent clears its head, not answered from memory (:18863).
  • Views, one for each kind of reader (src/framework.ts:3453–3466), 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, :3443).
    • A resident's view (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).
    • An isolated agent's view (isolatedView, src/framework.ts:3459) leaves out legacy reasoning-loss notices and, as a resident's does, change rows stamped as another agent's. So an isolated agent reopened while it observes another agent's slot compiles without the other agent's changes. A host's unstamped change stays in, and the agent's own namespace holds only its own rows and the host's.
    • An observer's view of the residents' slot (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 (:19123).
    • A derived context's view (derivedView) keeps what its source's view keeps, plus its own stored context injections and its own change rows. The live shared derivation (:12852) and the reopening of an ended agent's context (reopenAgentContext, :12660, through derivedViewOf, :12793) build the same views, so a fork resumed after it ends compiles what it compiled live; an inspection of it also shows the change rows its view keeps, which a compile on 'head' leaves out (:12669). 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:651): 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:3017, and :2994 for a move the budget forces). The framework's two copies stamp them as the copy's own: deriveAgent's copy mode (src/framework.ts:12368) and a fork's seed (:21141). A shared child deriveAgent makes already counted its parent's through selfParticipants, 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:12197, :12879), and as it resumes one, from the records its view is composed from (:12583, sourceChangeOwnersOf, :12745). 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 request, so it can reach a fifth before or after its parent. In a run reported with the review of c045b9870b, a parent whose requests carried a non-stored afterUser injection 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:3478–3479), so its source's aren't sent.
    • This does what the first of agent-framework#887's two options proposes, an issue FableDendrite filed: a reopened fork's view is composed from its source's view whatever slot the source reads, with a test that reopens a fork of an agent on an isolated slot. That test, "a fork of the subconscious, reopened after it ends, shows what its live compile showed" (test/dendrite-fork-view.test.ts:220), fails with the src of 073132e851, this head's merge of 0f11975e48, apart from src/tune-out/tools.ts (red first, under "Tests").
  • The subconscious's own voice is its name alone, as main makes it (src/framework.ts:19156–19157). isOwnParticipant decides whose turns are presented as an agent's own and whose change rows are its own (src/agent.ts:622). A subconscious on another model than its principal declares on its head, since toolDeclarations is among the fields it doesn't take from its principal then (SUBCONSCIOUS_MODEL_FIELDS, src/framework.ts:4326). A reader fork the subconscious runs on another model than its principal declares on its head too: invokeReaderFork sets toolDeclarations: 'head' in the fork's own config when the fork's model differs from the principal's, compared exactly (src/framework.ts:19367). A reader fork on its principal's own model keeps the principal's setting.
  • A resident's registry record now holds its selfParticipants and presentAs (src/framework.ts:19035–19036), 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:12218, :12225), with a spec and without one (test/dendrite-fork-view.test.ts:346). 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:298), and the resident's record holds its configured selfParticipants and presentAs (:321).
  • resumeAgent reopens the context with the mapping the resumed agent runs with, an override's selfParticipants, else the record's (src/framework.ts:12566), so its view and its head read one mapping. It gives the resumed agent a head store of its own in memory, per branch (:12585), as every ephemeral agent has. A test shows a head: trace after a resumed job's first request (test/dendrite-lifecycle.test.ts:712).
  • Forks start from the change rows in their template's compile and from its recorded head, read with the template's compile its copy is taken from (src/framework.ts:21137), so the head and the rows the fork copies are of one moment. The template's own change rows are copied as the fork's own, and a host's change row as no agent's own (:21141).
  • An agent that continues another's context: createEphemeralAgent(config, spec) (src/framework.ts:12099), when spec.inherit names a running agent that declares entries, starts the new agent with that agent's recorded head, read with the copy it seeds where the framework copies that agent's context, at deriveAgent's copy (:12363), and otherwise as the creation begins (:12121). For a shared child that's the checkpoint's instant: nothing awaits between that read and context-manager's derive, which takes its checkpoint before its first await (0.27.0's src/context-manager.ts:2949–2972). 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:550). deriveAgent creates its child this way, and its copy mode stamps the parent's own changes as the child's (src/framework.ts:12368). A host that copies another agent's messages itself passes a spec whose inherit names that agent, and stamps the change rows it copies with the metadata systemChangeMetadata(config.name) returns. The unreleased headFrom option, which named the agent by hand, is gone.
  • A shared child deriveAgent makes counts its parent's change rows among its own, since deriveAgent makes the parent one of its selfParticipants. 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.
  • At a tool boundary, a row stored for the agent goes into its live turn only where the agent's own view filter keeps it, and a change row only for an agent that declares entries (requestCarries, src/framework.ts:26005, at :19794). The hand-off's check reads the same predicate, so a row the boundary presents and the request leaves out, such as a host's change at an agent on its head, doesn't break the order of what the stream was given (handOff, src/presentation.ts:1028; src/framework.ts:20129). For this, each context manager's view is recorded with it, its sees beside its filter (recordView, :26014: :19020 for a resident, :19132 for 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 (:19873).
  • A change row is never a message said to the agent, whatever its metadata (isConversationalRow, src/framework.ts:493): it starts no re-send count again under main's exhaustedTurnRearmMs and clears no earlier deadline cut of a re-armed turn (:34553), counts as nothing heard (:26033), moves no turn to a channel (:25246), isn't where a turn's input pin starts (turnInputs, :1713), isn't new input to the turn when it's handed to the live stream at a tool boundary (:19869), and isn't announced as someone speaking by the notice for a message addressed elsewhere (:19940).
  • Other readers of stored messages:
    • host /undo N counts 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:15956). In a presented window it counts the agent's own rows of that window (:15860–15870), and in a window still in legacy mode, the record's (:15872);
    • in a window still in legacy mode, /undo N's count (indexBeforeLastMessages, src/framework.ts:1241) reads the store's tail through scanFromTail (src/stored-removal.ts:20), as locateMessageIndices and the change guard do. It reads newest first, in blob-free windows of 16 rows doubling up to 500, until the reader is done;
    • a rollback refused because rows one write presented with its target follow it counts and names the messages among them, never a change row, which it no longer advises suppressing, and one that change rows alone follow is refused saying so (src/framework.ts:18140–18144);
    • a surgery preview's neverPresented counts a row as presented only where a live agent's request carries it (holderNames, src/framework.ts:18458), so a change row that only agents on their head hold counts as never presented;
    • a batched wake's line is left out while the newest row in the agent's window that a reply could be meant for is one of the wake's rows (batchIsNewest, src/framework.ts:35416). main's check passes over clock rows, stored context injections and the batch's own line, among the newest 32 rows (NEWEST_SCAN_ROWS, :4876). In a window still in legacy mode, it also passes over rows the agent's view leaves out. This pull request has it pass over change rows too, in either mode (:35423), and keeps the 32;
    • the poisoned-history shed passes over every change row, stamped or not, as it does a row marked system (src/framework.ts:15156);
    • history's tools show a change as [system change], and in raw format the block without what it declared (src/modules/history/index.ts:2804), as extract's description says, since in the shared slot it may be another resident's, whose tools and system text are its own;
    • a module's view lookup (viewKeeps), which history's window marks read, answers what the agent's request carries: its view's rule, and on the head no change row (viewKeepsFor, src/framework.ts:35570).
  • isSystemChangeMessage and systemChangeMetadata are exported from the root, for a host's own readers of stored rows and its own forks (src/index.ts:9).
  • Traces: head:declared, head:moved and head:resend (src/types/trace.ts:124). The changelog files them as breaking for a host that covers every TraceEvent['type']. A move of an agent's head that takes its own change rows out of the record retracts them from each other agent's window that held them, as presentation:retracted with a new reason, 'head-moved' (:1675), and the head:moved trace after it says which move it was.
  • Text: the settings' docs, selfParticipants' docs saying it also decides an agent's own change rows (src/types/agent.ts:503, src/dendrite/types.ts:283), createEphemeralAgent's doc on what spec.inherit gives, thinkingBinding.binds' three states in one paragraph, the README's Agents section, and the changelog fragments changelog.d/tool-declarations.added.md, .breaking.md, .changed.md and .fixed.md. The settings' doc and the breaking entry say that membrane sends a change only where the formatter's rendersSystemChanges and the adapter's sendsSystemChanges both hold, which a wrapping adapter must declare too. 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:179–187). main's exhaustedTurnRearmMs doc now says that a system change isn't a message said to the agent, the .changed fragment says such a change joins what 0.29.0's re-arm entry lists as not a message said to the agent, and src/silent-heartbeat.ts' description no longer says which reply follows a [Continue], since its sentence on that no longer held. Four of main's comments lose a sentence this change made false: the shed's doc, and the line beside its loop, no longer say that a row the agent is never sent is passed over, since an 'entries' agent is sent a host's change row the shed passes over; the RFC-014 note on a server's dynamic tools no longer says that every change to the list rebuilds the prompt cache; and holderNames' doc no longer says that a row an agent's view leaves out is held by no such agent, since it now asks what the agent's request carries.

Who needs to act

  • A host that wants it sets toolDeclarations: 'entries' for agents on models that take mid-conversation system messages through membrane's native formatter and an adapter that declares sendsSystemChanges, and sets thinkingBinding.binds where 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.
  • A host whose own change rows reached its model on main loses them on every agent on 'head', the default, and sets toolDeclarations: 'entries' for the agents that should send them. No setting sends them to the persistent subconscious, which on main carried them, and the direct API sends them only under 'entries', as the agent's requests do. A module's view lookup (ModuleContext.viewKeeps) follows the requests: for such a row it now answers false wherever the agent's request leaves the row out, an agent on its head included, where it answered true. The changelog files this as breaking (changelog.d/tool-declarations.breaking.md).
  • A host that stores change rows of its own, without an agent's change metadata, removes them itself. Under '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.
  • A host that handles every presentation:retracted reason, as an exhaustive switch does, handles 'head-moved' too, or lets a default branch take it. The changelog files this as breaking (changelog.d/tool-declarations.breaking.md).
  • A host that wraps membrane's provider adapter forwards 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' too

  • A request's added work on 'head', measured: a default agent's request build now copies its head's tools as JSON (copyHead), scans its compile for change rows of its own (ownSystemChanges) and reads its recorded head. At 80 tools (105,581 characters of JSON) the medians of these summed to 0.48 ms per request, against a median request build of 1.5 ms; at 160 tools, 1.10 ms against 1.9 ms. Nearly all of it is the copy. The build here is the default strategy over 300 short stored messages, with no provider call: a real strategy's compile and the provider's round trip weren't measured. The copy goes with the request; under 'entries', the framework keeps one recorded head per agent, about the copy's size (about 262 KB at 80 tools, 478 KB at 160). These ran at 39dc155cfd, on Linux with Node 24.22.2, in a run that waited for a CPU for 31% of its time, so the milliseconds may read high; the shares come from the same run.

Where an agent on 'head', the default, has change rows in its context, such as a host's, these change what it does:

  • a host that opens its context itself with residentContextOptions(name) reads no change row, as its compile doesn't, where main's shape keeps them all; subconsciousContextOptions leaves out every change row not the subconscious's own; and an inspection's reopen keeps a host's change rows and the agent's own (src/context-options.ts:4–7, :86, :123);
  • its requests, the direct API's included, and its compile leave change rows out ("On the head", above), where main's requests carry a host's change rows as stored, and membrane 0.8.0 sends a request holding one only through a formatter and an adapter that declare they render and send system changes, refusing it otherwise (dist/membrane.js:1381–1386); of membrane's own, NativeFormatter and AnthropicAdapter declare them (dist/formatters/native.js:112, dist/providers/anthropic.js:251);
  • a change row is never a message said to it, and none stored at its tool boundary goes into its live turn ("A change row is never a message said to the agent" and "At a tool boundary", above);
  • /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:

  • a change row after a batched wake's message doesn't make the wake write its line, where main's check reads the change row as newer (src/framework.ts:35423, and :35193–35195 at 36b6dfe9de; "Other readers of stored messages", above);
  • its request's tools are a copy of the head's as JSON writes them (copyHead, src/system-changes.ts:174, at src/agent.ts:2377), where main sends 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;
  • kv-unified's hash takes its tools as the wire carries them, leaving out a field whose value is undefined and taking a value JSON writes as text, such as a Date, as that text (headHash, src/agent.ts:188), where main hashes the tools it's given (src/agent.ts:2141–2145 at 36b6dfe9de), writing a field whose value is undefined as null (src/stable-hash.ts:11 at 36b6dfe9de);
  • a resident's registry record, and an ephemeral agent's, holds its selfParticipants and presentAs ("A resident's registry record", above).

With main

This head has merged main thirteen times. The last, 623530fe1a, merges main at 36b6dfe9de, 1,092 commits past the previous merge's a25e38b1bf. They bring #844, step 3 of #291: one record, with a presentation index for each agent, under which an agent's window holds what was presented to it, rows carry their audience's stamp, and a removal goes through one funnel. It also splits the registry's own slots (ownSlot). They bring #972's batchIsNewest and #1006's dependency ranges too. Git reported conflicts in 12 files. The merge's message says how it carries this pull request's change rows onto main's stamps, removals, views and batchIsNewest, its tool boundary and its other readers. The merge ports this pull request's rules onto main's own mechanisms and adds no rule to them. Each change row the framework writes is stored through main's own-row writer (storeOwnRow), so it carries its agent's stamp. A move of the head takes the agent's own change rows out through main's removal funnel, which retracts them from other agents' windows (removeOwnChanges, src/agent.ts:3056). The views are built over main's view filters, and batchIsNewest passes over change rows. 4dc473c5f6 names the retraction's reason 'head-moved', and e33af3b595 cuts or rewraps comments of main's that the merge left untrue. b577bf2e72 drops this pull request's own copy of a fix main already has from a1b8ca7e3f, the subconscious's own voice: two lines, a changelog fragment and a test. Each earlier merge's message names how it resolved its conflicts. Where the merges meet this pull request:

  • At the stream's error event, the fail-safe decides first and resends a request refused for its change at once on the head (src/framework.ts:30878, :30897). Otherwise, if no provider hold has taken the turn's retry (:30882) and no stated wait is held (:30888–30892), the error policy is asked with policyAttempt, which is cuts ?? attempt (:30896, :30899), unless a stated wait has failed or main's rule for a re-armed turn ends the turn (:30898). cuts is 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 (:30894). :30900–30901 keep that count when cuts is defined and the turn is retried, and clear it otherwise.
  • The direct API's request, under 'entries' with a recorded head, asks of its caller's tools and system text the question settleHead asks, through the one helper (ownDifference, src/agent.ts:2969; asked at :2050). Where nothing differs from what the recorded head and the agent's own change rows declare, the request goes on that head with every change row, as the agent's own requests on that head do, and its compile is bound under that head (:2044). Elsewhere it leaves out the agent's own change rows, and on its head every change row (:2060–2061), then presents the agent's own turns under its name. There, with thinkingBinding.binds: true, it compiles again, bound under the caller's head with a mark no activation's identity has (:2052, :2472), so the thinking before it is set aside, and so does the agent's next request: a loss, not a refusal. With binds left out, the compile isn't told the binding (src/agent.ts:2461), so nothing is set aside, and such a direct request carries the thinking signed after the first change row it leaves out, and all of it where the heads differ, under a prefix it wasn't signed under: with thinkingBinding.dropBlock the provider drops it, and without it an account that enforces the binding refuses the request, and one that doesn't decides for itself.
  • createEphemeralAgent takes the registry's spec second, as 0.30.0 released it, and the spec's inherit is 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:1713), 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:13452). The store keeps the head.
  • feat(gate): suppress rules keep an agent's chosen events out of its context #975's suppressed rows stay out of every view this pull request builds. A resident's view reads residentViewFilter (src/context-options.ts:58), which leaves them out (:63). The subconscious reads held rows, and observerView gives it subconsciousViewFilter (src/framework.ts:3457; src/context-options.ts:69), which leaves them out too (:73). viewHidesSuppressed asks an agent's view through storeViews, the map of views this pull request keeps (src/framework.ts:41896).
  • feat(prose): locked prose routing and the prose_locus tool #969's prose_locus tool joins an agent's tools while the agent is under locked prose routing (src/framework.ts:9918). Under 'entries', its arrival or removal is declared as any change to the agent's tools is.

What each part is for

  • The head, kept between moves: a provider caches from the first byte, and the head is the first thing a request sends. Keeping it unchanged between moves is what lets a change cost only its own bytes.
  • Changes in the context, where they took effect: they render where they happened on every later request, so the cached prefix holds. Stored, not recomputed, so a restart sends the same messages.
  • A declared change compiled again: the compile a request goes out from is what context-manager's next compile compares with, so a change appended after it would read there as rewritten, and a consolidation the change brought to a tenth would then move the head as if for free. Compiling again once the change is stored costs one compile per declared change, and the request then carries the change as the compile renders it, which is also what the thinking-binding record compares with.
  • The multiset rule for system text: a new block can be said as an addition; a changed one can't, since applied again it would be there twice. A reorder says nothing new.
  • Canonical tool comparison: a definition whose keys come in another order is the same tool; without it, every such reorder would be declared.
  • The record before the removal, and copies at the edges: with the removal first, a write that failed left the store's old head without the changes the move had taken out, and after a restart or a branch switch the next request could declare a tool after the calls that used it. With the record first, a write that fails moves nothing. What's recorded is a copy of what the caller advertises, and a request carries a copy of the head, so the head changes only when the agent records it. Were the request's tools the cached record's objects, a hook or caller changing them in place would change the record, and the next request would declare a change back. Every request compares before it goes, so a record and a context that disagree cost at most one write.
  • Only the agent's own change rows counted: a move removes only the change rows whose change metadata names one of the agent's own participants. Counted by their block alone, a host's change row would count though no move removes it. It would be folded and priced at every request, and could force a consolidation, with a whole write, each time. Counting only what the agent can remove keeps the count and the removal the same. It needs each compiled change's stored rows, since a compiled message carries no metadata; the compile already names them. The other way, removing every change row the agent's view shows, would take another resident's or a host's rows out from under them.
  • Its own by its participants, not by its name alone: a shared child of deriveAgent presents 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 and deriveAgent's copy read one rule, so no agent's view hides a change row of its own as another's, and an agent never folds a row its view hides. The removal reads the store by ownership alone, so it also takes out an own change row that the view hides for another reason, such as a suppression. 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.
  • What the request already carries, left out of what's declared: a host's shared child, or a copy a host made without stamping, starts on its parent's head and carries the parent's change rows, which aren't its own. Without this, it declared the parent's change again, and its request carried the change twice. Counting others' rows into the declaration instead would make a host's row that adds system text the agent doesn't hold read as a replacement, a move of the head at every request, since a move takes out only the agent's own rows. And a host's row defining a tool the agent lacks would have the agent declare that tool removed.
  • The head store, a state per agent, per branch: a write covers one agent, so a system injection that changes at every request writes that agent's head alone, and a write of what's already there isn't made. Read once per branch, since the framework writes it alone, and the branch is known by its id, since a name can be used again by a branch made later. Registered at its first write, so an undo's branch keeps it and an agent on its head registers nothing. A closed fork's head leaves memory, since its name isn't used again and a long-lived branch would otherwise keep every retired fork's head. A record that isn't a head is set aside rather than used, so an agent on 'entries' adopts again with one write rather than building a request from it, and the log names a record that can't be read once a branch, since every request on the head asks for it. An agent built without the framework, and an ephemeral agent, which doesn't outlive its run, keep their heads in memory.
  • thinkingBinding.binds in 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.
  • The binding's digests of the head that goes out: under '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.
  • The compile again on the head after its rows go: the record compares compile output, so a compile that held rows the request left out, and that no later compile has, would make the next compile strip the thinking produced since, the fail-safe's resend included.
  • Change rows out of a '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.27.0's src/system-changes.ts:29, 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.27.0's src/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.27.0's src/context-manager.ts:2987), which would hide an 'entries' child's own rows under a 'head' parent with no view.
  • On the head, the record cleared before the rows: the other order would leave an old record without its rows whenever the clear failed, and a later 'entries' period could then declare a tool after the calls that used it.
  • Characters on both sides: the thresholds compare the changes' characters of JSON with the request's, both from the request the compile builds, so no count of tokens is estimated, and an agent built again doesn't wait for a response before it consolidates. Both sides are the rows as the request sends them, read by one helper the request is built from, so what transcript mode or the sanitize pass leaves out of the request is left out of the share, and its free-move test sees only thinking that's sent.
  • A declaration the store holds declared: a row the store holds is in the window the next compile reads, so a request sent from the earlier compile would lack a change its successor reads as already sent.
  • The rows taken out when no head can be recorded: with no head recorded, every later request adopts again, so the rows declare nothing a request uses. A compile filtered after context-manager committed it would leave its record holding rows the request never carried, and with binds every later reply's thinking would go at the first compile without them.
  • The hand-off's predicate: the boundary's filter and the hand-off's check read one rule, so what the stream hears and what the window's tail is compared with can't drift.
  • An isolated agent's view with the resident's rule: one rule for every view an agent's own namespace can hold, so an observer of a slot other than the residents' never compiles another agent's declarations.
  • Memory heads per branch: a live agent can span a branch switch, and a head read on a branch it wasn't recorded on would declare against another history.
  • Consolidation: without it, every request reads every old change for as long as the agent lives. Waiting for a compile that already rewrites the changes keeps the move's own write to the head and what precedes the first change. The binding rule keeps it from setting aside thinking the compile kept.
  • The forced moves: for when that free compile doesn't come. The fifth bounds what old changes cost every request; the fallback at the refusal itself covers a budget that leaves the changes less room than a fifth, without plumbing each strategy's budget into the agent. Rows that no longer fit block the compile wherever they're kept, on the head and before a head is recorded as much as with one, so the fallback takes them in each. With thinkingBinding.binds: true, both set a binding model's thinking aside, which is the price of not stopping, and the operator line shows it. With binds left out, the requests after them carry that thinking under a prefix it wasn't signed under: with thinkingBinding.dropBlock the provider drops it, and without it an account that enforces the binding refuses them, and the operator line names both settings.
  • The fail-safe: a model that stopped taking changes, or refused one in words membrane doesn't know, would otherwise fail every request carrying it, for good. So a 400 on a request carrying a change resends on the head, binding model or not, unless membrane files it as context overflow or an account refusal. Those are words membrane recognises as saying something else, and a resend on the head would be about as large, or come from the same account, while keeping the agent to its head until it's built again. That costs the one case named under "The fail-safe", above. An unrelated 400 on such a request then costs a head move, and, with thinkingBinding.binds: true, the thinking of the replies signed under a prefix that held a change row its requests on the head leave out. After a binding agent's first change, every request carries a change until the head moves, so any 400 in that time costs this. The alternative is an agent whose every turn fails. The operator line, raised at each resend, shows that cost. For a refusal that membrane recognises, it names the remedy, since the host then set 'entries' on a model that refuses system changes, or sends through a formatter or adapter that can't send them. For a 400 that membrane doesn't recognise, it says the 400 may not be about the change. The resend is the same turn's retry, so the turn keeps its undo checkpoint and tool-call count, and the refused request counts as an attempt, whether membrane refused it before sending or the provider refused it.
  • The fail-safe ahead of 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.
  • The operator lines' word on thinking: said outright only where the host declared a binding model. With binds left out, the agent can't know whether the model binds, so the line says the thinking is left out only with thinkingBinding.binds: true, and what follows without it, with thinkingBinding.dropBlock and without.
  • The views and history's withheld declarations: without them, two residents would read each other's changes as their own and append corrections to each other at every start, and a resident's history would show another's tools and system text. Built once for each kind of reader and composed for a derived context, the view a fork has live and the view it's reopened with are one, so resuming a fork shows what it compiled live, and inspecting it does too, except that an inspection also shows the change rows its view keeps, which a compile on 'head' leaves out.
  • The fail-safe for a later round handed a change: the change was handed to that round, so its refusal can be the refusal of a change the fail-safe is for. Left to the error policy, where a 400 is terminal, it would end the turn.
  • The boundary asking each agent's own view filter: what a tool boundary injects into the live turn is then what the next request sends, for any view filter, the subconscious's included. Asking sees alone, 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.
  • A change row never said to the agent: a change alters the request, not the conversation, so it starts no re-send count again, as the heartbeat's tick doesn't, clears no earlier deadline cut of a re-armed turn, counts as nothing heard, and isn't where a turn's input pin starts, which would pin the range back to the change. noteSpokenTo and noteArrival keep sees: they count only rows that are said, which a change row never is, and each view's filter differs from its sees only on change rows (src/framework.ts:3454, :3457, :3465).
  • The fork's head and copied rows, and the head from 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's spec.inherit already says a new agent continues another's context, so it gives a host's subagent the same without exposing heads in the public types, which headFrom did by hand. The head is read at the instant the rows the new agent continues are taken, at the copy or, for a shared child, before the derive with nothing awaited between, so the two are of one moment: a head read earlier costs a declaration, and one read later could 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.
  • The subconscious on another model: a host sets '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:1300), so it keeps the principal's setting.
  • A resident's record with its mapping, and a resume reading the mapping it runs with: a reopen rebuilds the view from the records, so without the mapping, a fork of a resident reopened after it ended left out a change stamped with the resident's former name that it kept live. A resume under an override reads the override's mapping, so its view and its head agree.
  • A resumed job's head store: without one, no 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:43). It reads the agent's own name alone, not its selfParticipants: 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:695).
  • The wait where context-manager would refuse the change: the store refuses such a write anyway, and a guard that read less would let the write throw at a sharded or partly answered call, and hold back a change after a cycle its own blocks answer. Reading the rule from the tail keeps the read to the last few rows. It reads the context manager's window directly, with no check that it can, since context-manager ^0.27.0 always has one (getMessageWindow, src/context-manager.ts:1855 in 0.27.0). 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 real MessageStore, so a difference that changes the answer for one of those cases fails CI.
  • kv-unified's hash and the dry run: kv-unified keys its layout on the head that goes out, so the compile follows the head; a dry run shows the request as it would go, storing nothing, and its compile's sources stay index for index with its messages.
  • The direct API: it declares nothing, as before. Where what its caller passes is what the agent's recorded head and own change rows declare, its request is the one the agent's own requests on that head send, every change row with it, so thinking signed after those rows, or after a host's change, goes out under the prefix it was signed under (src/types/agent.ts:210–230). Sending the caller's head without the agent's own rows there, as it did, sent the thinking signed after them under another prefix, which an account that enforces the binding refuses without thinkingBinding.dropBlock; a caller passing the tools the head and rows declare together lost that thinking too. Elsewhere it sends what its caller passes, without the agent's own rows, and the compile's mark sets the thinking aside rather than send it under another prefix. It asks the question through the helper settleHead asks it through, so the two can't disagree. With binds and a difference it compiles twice, as an activation does when it declares a change, since which case holds is known only from the compiled rows.
  • The settings' checks, the traces and the root export: a wrong value is refused before anything opens, naming its kind but never a string's value, which may be a secret pasted into the wrong field; the traces are how an operator sees a head move; a host reads stored rows by the same rule.
  • The tests' helpers:
    • The context-manager fakes in thinking-binding, thinking-binding-binds, thinking-drops, config-switches, activation-request-sanitize, agent-cache-ttl, kv-unified-wiring, assistant-persist-refusal and unsent-compiles report a branch (currentBranch), which an Agent's own head store reads, per branch.
    • stored-removal's rows is a synthetic window that records each read (offset, limit, resolveBlobs), so the tail scan's tests check it reads newest first in blob-free windows of 16 doubling up to 500.
    • tool-declarations' sizeRequest pads the first user row so a compile's request comes to an exact count, so the consolidation tests sit exactly at a threshold. Its fake context reports a branch (currentBranch), which a test switches, and its last committing compile (committed).
    • The fail-safe test's HostChangeAtTool stores a host's change at a tool boundary, and RefusedAfterChange refuses the round after it's handed one, for the test of a later round's refusal.
    • dendrite-fork-view's childAndGrandchild holds a shared child of mira mid-turn on a gate while its own child is made, shared or by deriveAgent's copy, so the grandchild is derived from a running agent whose open call and change rows it inherits.
    • The fail-safe test's rearmed harness 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, so main's exhaustedTurnRearmMs sends the turn again. Each later send fails with the error the test chooses, if any, and the policy retries an invalid_request failure, as a refusal is.
    • main's test of a turn's channel (test/shared-channel-id.test.ts) passes a row, as repinChannel now takes one.
    • Since this head's merge of 36b6dfe9de, a row the framework tests plant reaches the agent's window as a host's write does, one write for each row: store (test/tool-declarations-framework.test.ts:67) stores it and hands it to the presentation's afterStore. An ephemeral or derived child those tests build without running it is taken on (adoptPresentation, through adopt, :74) before its first request, as its run would take it on.
    • The fail-safe test's turns harness 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.
    • changeRow and the fake context's getAllMessages, getMessageCount and getMessageWindow give the agent-level tests stored change rows.
    • The fake context gives its compile's provenance a branch, since main's noteRenderedInjections keys the stored injections a compile rendered by branch.
    • The fake context's log of the windows read (windows, with each one's offset, limit and resolveBlobs) shows the guard's read stays at the tail, blob-free.
    • The fake context works out its firstChanged when the property is read (a getter, test/tool-declarations.test.ts:46), as context-manager's is. The getter counts its reads, and with 'compare' it compares with its last committing compile as context-manager compares.
    • priceOf prices a change as consolidation does, by its JSON's length.
    • digest digests a value as the agent does (as test/thinking-binding-binds.test.ts has its own), so the identity tests compare the digests with what the request carries.
    • The framework tests' Assembling names what the assembly returns beside its request.
    • The host-command test's stub rows carry content, as every stored row does.
    • The fail-safe test's turns harness, asked for a fork (asFork), stubs the registry so the resident has a home channel and doesn't own its provider scheduling, as a conversation fork doesn't, for the test of a refusal's stated wait (test/tool-declarations-failsafe.test.ts:209).
    • The binding tests' stand-in hashes a request's system text, tool names and messages before a block (prefixHash, with canonical leaving out cache markers), and misboundThinking finds a thinking block whose signature isn't that hash, as an account that enforces the binding refuses one. thinkingIn counts a request's thinking blocks.
    • RefusedTwoBoundariesOn counts a change handed to it only from its second boundary, so its refusal comes a round after a reply signed over the change, and refusedAtALaterRound can make context-manager's noteThinkingDropped answer false, for the arm whose note doesn't land.
    • directAfter runs a resident's turns with the tools each turn gives it, then a direct call with the tools a test passes, with or without binds, against the binding stand-in, and returns the turns' requests and the direct one; changeRows, definedIn and toolNames read a request's change rows, the tools they define and its tool names.
    • The framework tests' withFramework opens a framework on a new store with the agents and config a test gives, and stops it and removes the store after. forked copies alpha's compiled rows into a new child as the child's, its change rows stamped as the child's own, for the tests of a child's start on its parent's head. statesWritten records the id of each store state a run writes, so a test checks that one agent's write leaves the others' unwritten. forkOf captures the agent a reader fork derives and stubs its run, so a test reads that fork's head. holds checks an agent's context manager holds each row a test stored.
    • system-changes' parity test writes each case's rows into a real store, answering a call still open with a result before a case's change row, as the store requires, and removing those results after, so the store holds the case's own rows when its refusal is compared with the agent's check.
    • dendrite-fork-view's startOf presents an agent's window as a turn's start does.
    • rollback-cut-accounting's boot takes settings for scout, such as 'entries', and hostChange is a host's change row. continuing-start-silence's ErrorsAfter records whether a boundary handed it a change (handedChange).
  • Left out:
    • Consolidating at a lapse of the cache. A cache keepalive refreshes the cache between requests, so the time since the last request doesn't show a lapse, and a wrong judgment turns a free move into a whole write.
    • A block whose new text extends its old text, as an addition of the suffix. A live document is edited in the middle as often as at its end, a size cut changes its end, and the model would read a fragment apart from its section.

Tests

  • Fixtures: tool-declarations, tool-declarations-framework and dendrite-fork-view give their agents a system prompt of 78,000 characters past its first line, as a host's system text runs to thousands, so a test's own changes stay under a tenth of its request. Apart from the tests of consolidation, the largest change a test declares is 3,758 characters, in a request of 82,222. The consolidation tests in tool-declarations keep a short prompt and size their requests exactly. The two in tool-declarations-framework that test a forced move's operator line (test/tool-declarations-framework.test.ts:286, :321) keep the long prompt and declare a tool of 32,118 characters to pass a fifth, in requests of 110,368 and 110,469.
  • test/system-changes.test.ts:
    • recognising a change, and a damaged row with no content array as none;
    • the agent's own changes in a compile, by the metadata of each one's source rows (not another agent's, a host's, a summary, one with a host's row among its sources, one whose rows the compile didn't read, or one whose row names the agent but isn't marked a change, and none in a compile that names no sources);
    • where a change written at the end would split a call from its result, as context-manager's refusal reads the store, for a call last or past change rows, parallel calls with one answered and with each answered, a cycle its own blocks answer and one they leave a call open in, a call on a body's first shard, a shard that starts another body, another message after the call, an older call past an answered one, and nothing; and each of those built in a real MessageStore, which refuses a change at its end, in its own words, exactly where the rule says one would split a call (:199);
    • folding changes in order, a removal and a definition of one tool in one change included;
    • the difference, for tools added, removed and redefined (by schema alone too), system blocks added, changed, removed and cut, a reorder, a repeated block, an addition the head already holds, and a tool whose schema holds a Date, compared as its recorded head holds it.
    • requestCharacters: the head and the messages as JSON, every opaque payload left out, nested ones included, and text counted character for character; the fields a request on the native formatter doesn't send left out, while a tool's parameters of those names count.
  • test/context-options.test.ts, main's file: its comparison reads a system change row as a tag naming its tools, so change rows count in it (:28); with a host's change row and one each resident and the subconscious stamped as its own in the store, and one resident under 'entries', each exported shape compiles what its live agent compiles, the resident under 'entries' given its self participant and keeping the row stamped with it, and the subconscious's keeping its own change row and no other (:152); and an inspection's reopen of a resident on its head keeps the host's change row and its own, not another agent's, and otherwise its live view (:165).
  • 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;
    • adoption, a declared change and a restart; head:declared's fields;
    • an addition, then a replacement; a reorder; a tool's nested schema changed in place after adoption;
    • dry runs: a declaration, adoption, a replacement (with no change its head already holds, and its compiled sources index for index with its messages) and the head, storing nothing, and a replacement compiled again for kv-unified's hash;
    • the [Continue] case; a tool loop's prompt past a trailing change;
    • calls waiting for results, with another participant's message and another call's result after them; waiting after another's partly answered parallel call and a call sharded over a body, and declaring after a cycle its own blocks answer, and after a cycle the agent's own message answers (:323); a change stored by an assembly whose request never went out;
    • a head that can't be stored: with a head recorded, moving nothing and keeping the agent's change rows (:448); with none recorded, sending what's current, its own change rows taken out and its compile made again, so the compile it commits holds no row the request left out (:472, :483); a change row the store holds though its write threw, declared and compiled again (:546); heads in memory kept per branch, a branch without one adopting and a switch back reading that branch's head (:522); a preview's appended host change counted as carried, so nothing it defines is declared (:536); 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 (:496); 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;
    • kv-unified's hash, the same at adoption and after with a tool field left undefined, and the recompile after a move;
    • the direct API's request, which leaves the agent's own change rows out where no head is recorded (:777);
    • a preview's appended rows priced into the request, so a preview moves the head past a fifth exactly when the request with them would (:1155);
    • two agents adopting a tool with and without a field left undefined: the same binding digests and kv-unified hash, and neither declares a change when its tools take the other form (:343);
    • a second copy of a system block the head already holds, declared (:314); a consolidation free only at a compile that re-renders from the first row a move takes, a derived agent's source's included (:1106), and a derived agent holding only its source's rows consolidating by share (:1127);
    • through membrane's NativeFormatter: the change goes out as a system message after the user turn, the [Continue] one included;
    • the request priced as it's sent: a foreign round that transcript mode cuts to a line doesn't hide a change past a fifth (:1202), its unsent thinking doesn't block a free move (:1222), and a base64 source's data inside a tool definition counts on neither side (:1235);
    • consolidation, in characters of JSON on both sides: at a tenth and not below it; at a re-rendering compile, not after the changes; past a fifth at the first committing compile of an agent built again, before any response; a change at the very start; firstChanged not read below a tenth; a declared change 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; and a binding model's thinking, before firstChanged, at it, and redacted;
    • the forced moves: at a fifth and not below it, with firstChanged after 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 (:914), a derived agent's compile refused over budget with only its source's rows kept (:898), a derived agent kept to its head taking its source's rows out, also when its compile is refused over budget (:883), 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 (:934), on the head while the record can't be cleared, with the agent's own rows kept (:572), and on the head with no rows, the record left as it was (:582);
    • on the head, the agent's own change rows kept while its record can't be cleared, and both taken out once it can (:663);
    • a host's change row: not counted in what the agent declares, priced for no consolidation, left by a replacement and its dry run while the agent's own row goes, not declared again where its row already carries the change the agent's tools need, neither a removal nor system text a host's row already carries while a further copy of that text the agent holds is still declared (:631, :641), not where a consolidation's changes start, and on the head left out of the request with nothing traced;
    • thinkingBinding.binds in three states; with binds: true under '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;
    • the settings' refusals.
    • The fake context's compile names each row as its message's source, as a compile's provenance does.
  • test/tool-declarations-framework.test.ts:
    • a record that isn't a head set aside, with the log line saying an agent on its head sends no request on it (:1111); the subconscious kept from alpha's change once alpha has declared and stored it (:882); a record that can't be read, with alpha's own change row taken out of the request and its context (:1178); the requests beside bulky change rows, each once those rows are in the agent's context (:1438); and the change the cut keeps leaving the window for arrivals after /undo (:227);
    • a record set aside as damaged, cleared when an agent kept to its head deletes its head (:1135), and the failure line when an agent's first head can't be stored (:1154);
    • a conversation fork's record carrying its template's mapping (:853);
    • the last visible message after an undo, with a change a preview declares after it (:790);
    • an isolated agent that observes a slot other than the residents', reopened through reopenAgentContext with a view that leaves another agent's change row out and keeps a host's (:119);
    • a module's view lookup answering what each agent's request carries: another resident's change not kept, nor any change for an agent on its head (:145);
    • host /undo N not subtracting a change the cut keeps, stored before the target and shown after it (:227), and, on a window that is the record because its write-down failed, counting the record's messages and taking a change among them (:255);
    • a copied child seeded with its parent's head as the copy is taken (:752);
    • two residents in one slot, each with a change of its own, one moving its head, which takes only its own, while the other keeps its rows;
    • a resident's change row left out of a co-resident's window, as another agent's own row (:104), and taken out of the window of a resident that observes it when a move of its head removes it, traced as presentation:retracted with 'head-moved' (:163);
    • a restart;
    • a record deleted on the head and gone after a restart;
    • host /undo N's count and its refusal, and the last visible message it reports when a change follows it;
    • the operator lines, for a forced share naming the thinking as a condition with binds left out and outright with binds: true, naming none with binds: false, and for a move forced by the budget;
    • on the installed context-manager, with binds: true and 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;
    • a declared change's request built from the compile that wrote the record, so a note under it is taken;
    • built again on the head after an 'entries' period, the agent's record and its change row gone, and a noted reply after keeping its thinking (:361); 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 (:392);
    • a state write that throws, request after request, moving nothing and keeping the change with the recorded head (:1004);
    • a 'head' agent's compile leaving every change row out (:1431): 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;
    • a fork, and its record staying in the store at close and leaving memory;
    • a fork's head read as its copy of the template is taken, so a head the template moves while the fork's context opens is the fork's head, with nothing declared (:838);
    • an agent that continues another's context by createEphemeralAgent's spec.inherit:
      • the parent's prefix and nothing declared;
      • a host's shared child, whose own participants don't include its parent, and a host's copy made without stamping, each carrying the parent's change once;
      • separate records, neither one's move reaching the other's;
      • the child's head kept as recorded when what the parent's request carries is changed in place;
      • the parent's head read as the creation begins, so a head the parent moves while the child's context opens costs the child one declaration;
      • nothing from a parent on its head, from an inherit at a checkpoint the caller names (atSequence), or from an inherit naming no running agent;
      • a shared child taking its own change rows out, on the head and at a move of its head, from its own branch alone, as its branch's stored rows show before and after, leaving its parent's context as it was;
      • a host's shared child on its parent's head declared what its parent's rows say that it doesn't hold, in both directions (:507), and a dry move of a host's shared child showing none of its parent's rows, under either of its parent's names (:533);
      • a host's shared child with its own prompt taking its parent's change off its branch at its move, its request carrying only its own head (:489), and one at a checkpoint (atSequence) with its parent's prompt, tools and added block sending the block once (:550);
      • a shared child derived while its parent's call is still open, declaring its own change (:695);
      • a child deriveAgent makes, shared and copied, starting on its parent's head, its first request carrying the parent's prefix and declaring nothing;
      • a child deriveAgent copies, taking its parent's copied change out of its context when its head moves;
    • an agent's own change rows: one stamped with one of its selfParticipants, kept in its view and counted as declared; and a host's row whose metadata names an agent but isn't change metadata, in every resident's view;
    • a host's row that adds system text a resident doesn't hold, carried as stored, with no move and no declaration over two requests;
    • the subconscious, and on another model;
    • a reader fork: on another model than its principal, its request carrying no change row and the tools as they are, while the principal's next request still carries its change (:1289); on its principal's own model, its request carrying the principal's change as stored (:1300); and of a principal on its head, on that model, declaring on its head too (:1309);
    • a resident waiting to declare while the slot ends on another resident's call, and declaring right after its result;
    • the shed passing over a change row, stamped or not;
    • history's withheld declarations;
    • a branch switch;
    • isSystemChangeMessage from the package root;
    • the recorded heads in the store:
      • one agent's replacement writing its own state alone;
      • a state registered at its first write, and none for an agent on its head;
      • no write of the head already recorded;
      • each agent's record read once a branch;
      • a branch's own record read though one of the same name came before it;
      • a record that isn't a head set aside and recorded again;
      • a state that can't be read sending the current head, logged once, and written again;
      • another agent's head untouched each time;
    • a host's change row stored without an agent's metadata in two residents' shared slot, in each one's request and not in the subconscious's, and left where it is by a resident's move;
    • a resident's request ending on its own turn past a change and then one of 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;
    • on the head, a resident's request ending on its own turn with [Continue] last though a host's change row follows it, and the same for a resident kept to its head (:945);
    • a fork of a template whose slot holds a host's change row and the template's own, where the fork holds the host's as no agent's and the template's as its own, and the fork's move takes its own and leaves the host's;
    • the subconscious keeping its own declared change in its view, so it declares it once and stores it once.
  • test/dendrite-fork-view.test.ts, main's file: a live child takes its spec's mapping when its config names none (:363), and a config's empty list stands, live, in its record and when it's resumed with none of its own (:384); a resumed child reads its record's, its live run on 'head' (:398);
    • a shared fork keeping its source's system changes and its own and leaving out another resident's, and, reopened after it ends, still leaving out another resident's, with its source's change and its own taken out of its branch and the other resident's left there (:184);
    • a fork of the subconscious, reopened after it ends, showing what its live compile showed;
    • the subconscious's compile and a fork of it showing a held row and leaving out a suppressed one, as a resident's does;
    • a host's 'head' child holding its source's rows and its own (:600), a 'head' child at a checkpoint, run to its end and resumed (:621), and a 'head' resident holding its own (:647), each compiling once when its compile is refused over budget and keeping its rows;
    • a host's shared child and grandchild, resumed with prompts of their own, taking every row their sources count as their own (:533), and a copy-mode fork of a host's running shared child stamping them as its own, so its move takes them (:551);
    • a shared child of a resident with former names keeping a change its source counts as its own under a former name before its first request, and holding none once it has adopted its own head, live and reopened alike;
    • a resumed child given the view of the mapping it resumes with, and one resumed with no mapping of its own given its record's (:398);
    • a job whose config names selfParticipants and presentAs, with a spec and without one, recording them (:346);
    • a resumed '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, once both have entered its window (:449).
  • 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-pointer.test.ts, main's file: a host's system change stored after a batched wake's DM leaves the wake with no line, in a presented window and in one still in legacy mode.
  • 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; locateMessageIndices finds ids across windows and stops reading once all are found.
  • test/tool-declarations-failsafe.test.ts, with a model that binds thinking to its prefix, and a stand-in that signs each reply's thinking with a hash of the request's system text, tool names and messages before it, and refuses thinking sent under another prefix: a host's change sent before the first turn, then a refusal of the next, recognised or not, and the resend and the next turn are answered (:515); a change handed at a boundary and a round after it refused sets aside the thinking of the round between (:493); a note that doesn't land, after a recognised refusal, raises an operator line saying that, where the head didn't move with the refusal, the agent's requests can carry that thinking until those replies leave its window or it's built again, and that without the drop control an account that enforces the binding refuses them; the resend and the next turn are refused (:504); and the direct API carries the host's change and the reply's thinking signed after it, so its request is answered (:565). A recognised refusal whose note landed raises the operator line, which doesn't say the note failed (:493). The direct API on an 'entries' agent, by what its caller passes: with its own change rows netting to nothing, it goes on the recorded head with both rows and every reply's thinking, with binds: true and without it (:699, :708); passed the tools the head and its row declare, on the head with the row and the thinking (:716); passed the head's tools while a row adds one, with no row and no thinking, offering only what it was passed (:725); passed a tool neither holds, with no thinking (:734). Passed its tools and one more, with a host's change row in its context, the direct request goes on the caller's tools and carries the host's row (:609).
  • test/tool-declarations-failsafe.test.ts: a resident whose tools change between turns, with the second turn refused: a recognised refusal resends 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; membrane's own refusal before sending, with no HTTP status, resends too, and the agent keeps to its head (:225); a failure of the resend reaches the policy as the turn's second attempt (attempt 1, counting from 0); a 400 membrane doesn't recognise resends and raises the operator line, binding or not, saying the thinking's cost with binds: true and none with binds: false, with its HTTP status; a refusal membrane recognises raises it too, with its remedy, and a third turn whose tools change again raises no second one (:248); and a 500 and a refused request without a change go to the error policy. In a turn main's exhaustedTurnRearmMs sent 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 (:202). 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 (:209). A 400 membrane files as context_length, and one it files as auth, 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 (:268, :276).
  • 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, which is presented in the window the pin is read from (:281).
  • 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, where the hand-off is vouched for all the same; 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 the boundary hands to the round after a send leaves the retry's line held (:504).
  • test/rollback-cut-accounting.test.ts, main's file: a surgery preview counting a change row that only an agent on its head holds as never presented (:353); a rollback refused for the rows presented with its target counting and naming the one message among them, not the change row (:364); and one that change rows alone follow refused saying so, advising no suppression (:380).
  • 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).
  • The fixes answering the review of 1973fd50ea, red first and each taken out alone, on Linux:
    • The direct API's question, at bf9594486d, where tool-declarations, tool-declarations-framework and tool-declarations-failsafe pass whole, 179 tests: with 78b52fe8d9's src/agent.ts, tool-declarations-failsafe fails exactly four, the two arms netting to nothing, the arm passing what the head and its row declare and the arm passing the head's tools while a row adds one. In that file, never going on the recorded head fails those first three and the host-row test; the identity without its mark fails only the arm passing the head's tools; going on the recorded head whenever one is recorded fails that arm and the one passing a tool neither holds; and today's request whenever binds is off fails only the arm without binds.
    • The rest, at 0f2e7b9302, where tool-declarations-framework, dendrite-fork-view and rollback-cut-accounting pass whole, 113 tests: with bf9594486d's src/framework.ts they fail exactly five, the empty list's test, the preview's, the two refusals' and the damaged record's for its log line. Before b258e51975 was committed, creation reading an empty list as none, the record keeping the spec's list, the refusal counting change rows and the preview asking the view alone each failed only their own tests in their file.
    • The operator line for a recognised refusal, at 8ca889bfcc before it was committed: with b258e51975's src/framework.ts, tool-declarations-failsafe fails exactly two, the once-a-build test and the later-round test whose note landed; with the line never raised, those two fail and so do the unrecognised 400's and the note's that can't land.
    • The shared encoder, at 0854ab7e3c before it was committed, where tool-declarations and system-changes pass whole: taking out the JSON round trip in the difference fails the new test, the by-value test and the test of a Date in a schema; in the binding's digests, the new test and the test of a dry move for a field set to null against one left out; in kv-unified's hash, those two and the test of the hash after adoption.
    • The exported shapes (Anarchid's R3-1), in the runs that built a147ffcbbf to bca9f4d181: each of these ran context-options alone, where it and the three tool-declarations files pass whole. 98088534f0's shapes, taking the new second argument and ignoring it, fail only its new parity test, at mira's shape; a resident's shape that ignores toolDeclarations, or selfParticipants, fails it at oren's; the subconscious's without its rule, or counting no row as its own, at the subconscious's. Making the reopen apply a compile's view without compiling fails only the inspection test, in its file.
    • slimepriestess's note at 0f2e7b9302, at 98088534f0's tree, where tool-declarations, tool-declarations-framework and tool-declarations-failsafe pass whole, 182 tests: with the direct path's filter dropping a host's change rows where it doesn't go on the recorded head, tool-declarations-failsafe fails only the test of a host's row through the direct API on the caller's tools.
    • slimepriestess's note, at 78b52fe8d9, 1973fd50ea with its test, where tool-declarations passes whole, 75 tests: with consolidation pricing the compiled rows without the preview's, only the test of a preview's appended rows fails.
  • Nine pieces of the commits answering the review of 7a1305b98c, each taken out alone and rebuilt before they were committed, on Linux, over the 27 files. Four ran in three later runs, each over a tree whose 542 tests pass with nothing taken out, and each failed only its own new tests: the fail-safe's note (the two recovery tests, the boundary arm and the note arm); its round rows (the boundary and note arms); the live mapping; and the last-visible preview. The other five ran over an earlier tree, where two tests failed with nothing taken out, both rewritten since: the last-visible test and "gives a child resumed with no mapping of its own the view of its record's". Beyond those two, each failed only its own new test: the direct API's rule; a fork's record; the set-aside record; the failure line; and an isolated view's rule (the reopened observer). A piece that also broke one of those two tests couldn't have shown it there. That tree's src/agent.ts is 8513aea411's, and its src/framework.ts differs from 8513aea411's only in the fail-safe's log and operator lines.
  • Eleven fixes for the review of 26d41c6192, each taken out alone and rebuilt before the last changes made answering it, on Linux, over the 26 test files the runs bullet names, whose 510 tests pass with nothing taken out. Each fails only its own new tests:
    • an isolated view keeping every row: the reopened observer's;
    • a landed declaration read as unstored: the declaration the store holds;
    • the view lookup by metadata alone: the view lookup's;
    • the share and the free test reading the compile as stored: the transcript share and the foreign thinking;
    • the change side counted unfiltered: the base64 inside a tool;
    • the hand-off checking every row: the 'head' case of a host's change at a tool boundary;
    • the copy's head read as creation begins: the copied child's;
    • the fork's head read before its copy: the fork's;
    • memory heads by name alone: the branch switch's;
    • a filtered committed compile with no head recorded: both "can't store a head with none recorded" tests;
    • a preview's rows not counted as carried: the preview's.
  • Five pieces of 26d41c6192's count, each broken alone and rebuilt, on Linux, in system-changes, tool-declarations, tool-declarations-framework and tool-declarations-failsafe, whose 173 tests pass unbroken:
    • The request's count leaving nothing out: only the two requestCharacters tests fail.
    • The changes priced at their JSON length over 2.3: 13 tests fail, each one whose change is meant to reach a tenth or a fifth of its request.
    • The request counted one character larger: 7 fail, each sized exactly at a threshold.
    • rawXml and unparseableInput counted: only the test of the unsent fields fails, at "none of them counts".
    • The fields left out on any object, not only a content block: only that test fails, at "a tool's parameters of those names still count".
  • Three of 623530fe1a's ports, each taken out alone at e33af3b595 and rebuilt, on Linux: in batched-wake-pointer and tool-declarations-framework, whose 117 tests pass with nothing taken out, each fails only its own new tests.
    • batchIsNewest not passing over a system change: the two tests of a host's change after a batched wake, in a presented window and in one still in legacy mode.
    • A head move's removal not retracted from other agents' windows: the test of a resident that observes the row, at "beta's window lets it go".
    • A declared change stored by a plain addMessage in place of storeOwnRow, and so without the agent's stamp: the test of a co-resident's window, at "and beta's leaves it out".
  • Red first, at dc8800379b, in its run above: with d44fc03d29's src/agent.ts, tool-declarations fails only the refused-row test, at its assertion that the refusal is logged.
  • Red first, at d44fc03d29, in its run above: with 5e4a561cc3's src/agent.ts, tool-declarations fails 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, and system-changes none. In those two files, loosening the guard's copy fails both rule tests and tool-declarations' wait test, and taking out either fix fails only its own test. Turning off context-manager's own refusal in its installed dist/src/message-store.js fails only the real-store test in system-changes, the one file that run covered.
  • Red first, at d61f9ea340, in its run above: with c045b9870b's src/agent.ts and src/framework.ts, dendrite-fork-view fails the three 'head' tests, each at an assertion, and tool-declarations and tool-declarations-framework none. 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.
  • Red first, at fe9b88bdcc, in its run above: with 48ede2f824's src/agent.ts and src/framework.ts, tool-declarations fails 3 tests (the child kept to its head, the free consolidation's check and the source-only share), tool-declarations-framework 1 (the child on its parent's head) and dendrite-fork-view 1 (the copy-mode fork), each at an assertion. With the dry filter, the resume's owners, or the resume walk's selfParticipants or depth each broken in turn, as the review of 48ede2f824 gave 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.
  • Red first, at 48ede2f824, in its run above: with 2289aef75a's src/agent.ts and src/framework.ts, tool-declarations fails the uncovered drop with the refusal it fixes, which reaches the test as the request's error, tool-declarations-framework 3 (the forced move's operator line, the host's shared child's move and the checkpoint child) and dendrite-fork-view 1 (the former-name test), and tool-declarations-failsafe none. The derived agent's budget move fails too, since 2289aef75a's code has no setSourceChangeOwners, so that arm's build exits 2 on that error and still emits. With each of the four breaks the review of 2289aef75a gave applied, its pin fails and no other test in those three files; with onHead || taken out of statedWait, 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.
  • Red first, at af040831ff, in its run above: with 76e2d43e48's src/system-changes.ts, src/framework.ts and src/agent.ts, system-changes fails 2 tests (the damaged row and the Date), tool-declarations 1 (the cycle the agent's own message answers), dendrite-fork-view 1 (the job's recorded mapping) and tool-declarations-framework 1 (the head store's log line, which now says what follows). With the agent's selfParticipants' turns read as its own in the open-call guard, tool-declarations-framework fails only the shared child derived while its parent's call is open.
  • Red first, at 2b0c223934, in the run above: with the src of cfb44201db, this head's merge of main 48f26ce56c before the commits answering the first review, tool-declarations-framework fails 7 tests, tool-declarations 5, tool-declarations-failsafe 2, turn-inputs-pin 1 and dendrite-fork-view 2, each of them a test those commits add, and the rest of those files' tests pass.
  • The commits answering the first review, 22 pieces each taken out alone at 2b0c223934 and rebuilt, in the same run, each failing exactly the tests predicted for it before the run:
    • A 'head' agent's compile: change rows kept in every 'head' filter fails the four tests in tool-declarations-framework of a request beside a host's change rows that alone exceed the budget, the inspection test, and both resumed jobs' tests in dendrite-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.
    • On the head: the record cleared only when the compile holds the agent's own rows fails the test of an agent built again on the head and "forgets a head deleted on the head for good: a restart under entries adopts afresh". No compile again after the removal fails the test of an agent kept to its head after a refusal. A record that can't be cleared counted as cleared fails the tests of own rows kept while the record can't be cleared, in tool-declarations and tool-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".
    • A head that can't be stored: the rows removed before the record fails both "moves nothing" tests and "sends what's current and keeps its own change rows while it can't store a head with none recorded". The request sent on what's current, not the recorded head, fails both "moves nothing" tests. A failed state write reported as stored, or the head kept in memory before its write, fails the framework's "moves nothing" test and its test of own rows kept while the record can't be cleared. The budget's move made though its head isn't stored fails "keeps its changes, and the refusal goes on, when a compile is refused over budget while its head can't be stored".
    • The fail-safe and turnInputs: a context_length 400, or an auth one, kept to the head fails its own test, and turnInputs counting a change row fails its test.
  • Red first and removals, at a91ad6d5d2, this head before its merge of 48f26ce56c: with 598f8cc58c's src/framework.ts, tool-declarations-framework fails 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 are withoutCarried's: its filter of removals a host's row already carries fails "doesn't declare again a removal a host's row already carries" in tool-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" in tool-declarations-framework. A resident's record without its presentAs fails the former-name test in dendrite-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.
  • Red first, at 405c86aaf0, this head before its merge of 84e0408977: with the src of 073132e851, this head's merge of 0f11975e48, except for src/tune-out/tools.ts, which was 405c86aaf0's, 405c86aaf0's tests fail 22 tests: 14 in tool-declarations-framework, 4 in dendrite-fork-view, and 1 each in dendrite-lifecycle, present-while-acting, tool-declarations and system-changes. The system-changes one calls ownSystemChanges with a predicate, where that src takes a name. continuing-start-silence, shared-channel-id, silent-heartbeat-rows and tool-declarations-failsafe pass whole.
  • Two parts of 7d78b4bb36's resolutions, each taken out alone at 67ba70e53d and rebuilt:
    • fix(rearm): take a re-armed turn's marked deadline cut as transient unless it's the second in a row #871's clear of an earlier deadline cut moved above the return for a row that isn't said: the two new cases in exhausted-turn-rearm fail.
    • noteSpokenTo judging a row by its metadata alone: six tests in exhausted-turn-rearm fail. 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".
  • The catch-up's 21 pieces, each taken out alone at 405c86aaf0 and rebuilt, and each fails a named test:
    • The views: a derived view without the child's own change rows, or with its filter read from its 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's includeHeld ignored 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.
    • An agent's own change rows: taking out only the rows stamped with the agent's own name fails both shared-child removal tests, and folding only those fails the one on the head. A resident's view by its name alone fails the selfParticipants test, a stamp read from any agent field fails the change-metadata test, and the subconscious keeping the primary's identity fails its test.
    • What the request already carries: the agent's own difference with nothing taken out fails both carry-once tests and the host's-row test in 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".
    • The starting head: no head from the spec's inherit fails eight tests, a head despite a checkpoint the caller names fails the atSequence test, 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, and deriveAgent's copy without the stamp fails the copy child's head-move test.
    • The rest: a resumed job without a head store fails its head test, and the notice for a message addressed elsewhere judged by metadata alone fails the notice test.
  • Three earlier sets of removals ran at earlier commits, not on this head's code:
    • Twenty-two pieces, each taken out alone at 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:
      • Forks, the request's tail and the shed: every copied change row stamped as the fork's own; change rows counted in the request's tail on the head as well as under 'entries'; the shed's check for a row holding a system change taken out.
      • The views: the subconscious's view hiding its own change rows too; a resident's view keeping another agent's change rows; the subconscious's view keeping change rows not its own.
      • isNewestIn: a 32-row bound on its scan; counting a row the agent's view leaves out; counting a system change.
      • The fail-safe: no check that the request carried a change; only the stream's first request read for a change; deciding after main's rule for a re-armed turn; that rule left out at the error event; the resend not kept to the head.
      • The tool boundary and what's said: the boundary asking sees in 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.
      • System text: systemBlocks without main's notice tag for a block that isn't text.
      • Two never ended, so each run was stopped after 240 s. With 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 on 9d2f62310e's files before the test runner failed its file.
    • Each piece of the commits from 9c5d59f4 to 6eb90104, taken out or weakened alone in the source at 6eb90104 and rebuilt, failed a named test (65 of 65). The pieces, as their breaks:
      • What counts as the agent's own change: every change counted as the agent's own; a change counted as the agent's own by its name alone; a change with a host's row among its sources counted as the agent's own; a change whose rows the compile didn't read counted as the agent's own; every change counted without its sources.
      • Folding, pricing and removal: every change folded onto the head; any change row setting off the removal on the head; every change priced; anyone's first change taken as the first; a dry run leaving every change row out; a move taking every row with change metadata.
      • The head store: one state for every agent; a state registered at its first read, or left unregistered; a write that changes nothing made; no write while a record is unreadable; a record that isn't a head used; an unreadable record logged at every read; heads kept across a branch switch; records cached by branch name, or read at every request; a delete that records nothing; a closed fork's head kept in memory; headFrom's head read before the open.
      • Reading the tail: isNewestIn counting 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; locateMessageIndices reading on once all are found; the change guard reading every window; the shed taking an unstamped change row; host undo counting a system change.
      • The binding: consolidation reading binds as declared; the record on with binds left 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 with binds left out, or named where binds is false.
      • The change guard: counting neither a cycle's own results nor results read on the way; not reading shards as one body; ending its walk at a change row; not waiting where context-manager would refuse.
      • Compiles and moves: a declared change appended after the compile; changes priced at a quarter of their JSON; the budget fallback only under '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; firstChanged read 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.
      • The request's tools: the request carrying the recorded head's own tool objects, or copying them one level deep; kv-unified's hash taking tool objects as they are.
    • Thirteen pieces of the merge of main at 4adbd600 and the commits after it, each taken out alone at a2d7bf7b and 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 what isNewestIn looks 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, endsOnOwnMessage reading past injections alone, change rows left out of the request's tail count, a dry run's appended change left out of it, and toolDeclarations left out of SUBCONSCIOUS_MODEL_FIELDS.

Under context-manager's strategies

At 740f4f979d, an earlier commit of this head, before its last six merges of main, the reader fork's setting on another model (c77a075fb4), and the commits answering the first review and the three 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.ts at 612ec8dbe3), with their token sizes scaled by 0.02 and the cache priced at 0.1 for a read and 2 for a write; and KnowledgeStrategy. 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.

  • The setup: a context budget of 6,000 tokens; for the strategies that take them, head, recent and chunk windows of 600, 1,500 and 500 tokens; and a user message of about 240 tokens each turn. At turn 4 a tool is added, its definition 300, 800 or 2,400 characters. Where the head moved for the change, this pull request priced it at 405 tokens for the 800-character tool and 1,100 for the 2,400-character one. Each strategy ran on a model that binds its thinking (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.
  • In every run:
    • every request from turn 4 on offered the added tool: its head held it, or it carried the change row;
    • every request that carried the change row, 518 in all, carried it unchanged, and the 198 of them that also held the message stored right after it held the change row before that message;
    • in those requests, no summary before the change row covered a message stored after it, and no summary after it a message stored before it;
    • no request compression made held a change row;
    • no turn failed.

When the head moved for the change, by strategy and the change's size:

strategy 300 characters 800, not binding 800, binding 2,400
passthrough never never never forced at a fifth or more, turn 5
autobiographical, flat or hierarchical never free, turn 11 (405 of 3,812 input tokens) never forced at a fifth or more, turn 5
autobiographical with kv-unified never never never forced at a fifth or more, turn 5
knowledge never free, turn 5 (405 of 2,174 input tokens) never not binding: free, turn 5; binding: forced at a fifth or more, turn 5
  • On the binding model, only the forced moves moved the head. The request at each of them held no thinking block, and no later request held more thinking blocks than turns answered since the move.
  • Where no free move came for the 800-character change (405 tokens), under passthrough and under kv-unified: passthrough's window first left out its oldest message at turn 16 (15 on the binding model), and kv-unified first folded at turn 18 (17), each with the change already under a tenth of the reported input. Every request from turn 4 through turn 32 carried the change row, the last at 7.2% to 8.1% of its reported input, under the fifth that forces a move.

These runs priced the change at its characters over 2.3 against that reported input, which weighs it about 1.74 times its share in characters. At this head each share here is about 0.57 of the one priced then: "405 of 3,812", 10.6% then, is about 6.1% in characters, under a tenth, so the autobiographical free move at turn 11 wouldn't come.

Summary merges past change rows. context-manager's autobiographical strategy merges two summaries only when no listed message stands between their sources (src/strategies/autobiographical.ts:7248–7249 in 0.27.0, and :6672–6673 in 0.24.2, which the runs below used), 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. At 48ede2f824, 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:

summary length a tool added every most rows at a sample, one every 20 turns moves request characters, 'entries' against 'head'
about 60 characters 10 turns 8 the adoption, 2 forced 19,399 against 19,608 at turn 200
800 characters 10 turns 14 the adoption, 1 forced 31,318 against 22,964 at turn 140, before its move; 26,112 against 24,312 at turn 200
800 characters 5 turns 13 the adoption, 2 forced 38,351 against 26,072 at turn 140, just after a move; 38,408 against 28,752 at turn 200

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. Those runs priced change rows at their characters over 2.3, against the input each response reported, the request's characters over 4. That weighs a change row about 1.74 times its share by characters, and this head counts both sides in characters ("Consolidation", above), so here the same rows reach a fifth later.

On Anthropic's API

At 2b0c223934, we sent one agent's requests to Anthropic's Messages API, once with toolDeclarations: 'entries' (E) and once with it unset (H). connectome-host at a1ef80ae, with one line added to pass the setting, ran the agent for four turns through its Anthropic path, on a package built from 2b0c223934 beside 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, to claude-opus-5-5, each as recorded but with streaming off, max_tokens 16 and its thinking setting left out. All 8 returned 200. What each turn was billed, as input tokens uncached, written to the cache and read from it:

turn E H
1 4, 4,981, 0 4, 4,981, 0
2 2, 1,776, 4,981 4, 6,713, 0
3 4, 1,775, 6,606 4, 1,625, 6,713
4 4, 1,625, 8,381 4, 1,625, 8,338
  • Turn 2: E's request keeps turn 1's tools and system text and ends with the added tool as a mid-conversation system message, and it reads all of turn 1's cache. H's carries the tool in tools and reads none.
  • E's turns 3 and 4: each request repeats the one before it exactly, apart from where the cache markers sit, and adds to it. Turn 3 reads 6,606 tokens, 151 fewer than turn 2 read and wrote (4,981 + 1,776), so the last 151 tokens of turn 2's prefix, which ends with the change, are written again at turn 3. Turn 4 reads exactly what turn 3 read and wrote (6,606 + 1,775). Why the API writes them again wasn't tested.
  • H's turns 3 and 4 each read exactly what the turn before read and wrote.

Not verified

  • A summarizer, and a real provider past the run above. None of the tests this pull request adds, and none of the strategy runs, sends a request to a model. Those that send at all send through a stand-in for membrane, and one test renders requests with membrane's own NativeFormatter without 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.
  • A host's change rows in the compile of an 'entries' agent the fail-safe keeps to its head. The fail-safe tests keep them there and check that the requests recover. None checks the budget they take, or a host's change presented at a later boundary while the agent is kept to its head, which reaches the compile and no request.
  • The cost of summary merges past change rows. The runs under "Summary merges past change rows" measure characters, with a stand-in reporting each input, and no provider priced them.
  • A conversation fork created by routing a channel's message, at the head resend. The test of a fork's stated wait runs an agent the registry answers for as a conversation fork, with a home channel and no provider scheduling of its own, as the router registers one (src/framework.ts:21176).
  • A conversation fork's compile refused over budget. The framework tells a fork, as it tells the others, whether its compile can hold change rows (src/framework.ts:21171). No test has one.
  • Other platforms. Every run named here was on Linux.
  • A resend that fails at a deadline or under a provider hold. No test or run has one. For a deadline's expiry, src/framework.ts:30894 sets cuts to feat(inference)!: cut an AgentFramework model call that stops making progress, and give a deadline expiry one fresh call #378's count, which :30896 gives the policy as policyAttempt, in place of attempt. :30880–30882 say "A hold takes the turn's retry" and check for a hold before the policy is asked.
  • What a fork of a 'head' template, or a child deriveAgent copies 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:21136, :12364).
  • Under the strategies, past the runs above: this head's code; real sizes; a change that removes, redefines or reorders tools; several changes, outside the runs of summary merges, which add a tool every 5 or 10 turns; the residents' shared slot; a fork; and WindowedPassthroughStrategy, which the subconscious uses (subconsciousContextOptions, src/context-options.ts:124).

Effie-2756 and others added 7 commits October 24, 2026 18:35
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>
@nissa-seru
nissa-seru force-pushed the feat/tool-declarations branch from 3274e6e to 73d9b82 Compare October 24, 2026 23:38
@nissa-seru
nissa-seru marked this pull request as ready for review October 24, 2026 23:39

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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:

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. isNewestIn still takes the newest row that isn't a clock row from queryMessages({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 skip isSystemChangeMessage rows the same way.
  • Medium severity, src/framework.ts:21690-21709. The fail-safe covers only stream error events, not a stream that throws. keepToHeadAfterRefusal is 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 a systemChangeRefused error 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) calls keepToHeadUntilRestart, and the next assembly then takes the !entries branch of settleHead, 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. readHeadDeclarations has three branches. It drops entries that aren't heads, it drops a stored record that isn't a map, and when getStateJson throws 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 says renderStats is 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 through copyHead, 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 own HeadDeclaration object under the fork's name, so two map entries share one object. Nothing mutates it today, but this undermines the invariant copyHead exists to keep. Use copyHead(templateHead).
  • Low severity, maintainability, src/framework.ts:992-1005. indexBeforeLastMessages reimplements the tail-window scan in stored-removal.ts. The new helper walks getMessageWindow backwards in fixed blob-free windows. locateMessageIndices in 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.

Comment thread src/agent.ts Outdated
Comment on lines 2231 to 2237
/** 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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment thread src/framework.ts Outdated
Comment on lines +12349 to +12358
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);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Effie-2756 and others added 12 commits October 24, 2026 20:47
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 slimepriestess left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.
  • isNewestIn must look past clock rows, change rows and injections. main's side also caps the scan at NEWEST_SCAN_ROWS ("past that many such rows it answers no"), while this pull request reads through scanFromTail. The two bounds need one answer.
  • In agent.ts, main's endsOnOwnMessage(messages, name, injectionTail) and messages.at(-1 - injectionTail) meet this pull request's lastConversational. 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 dist then npm 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 whose firstChanged is 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's system, tools and 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.
  • 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;
    • endsOnUnansweredCalls never 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.
  • The difference converges (Sonnet Weft's property probe, on the built pure functions): for any recorded head and current head, folding a change that declarationDifference returns back onto the head reads as none. 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 in agent.ts or framework.ts de-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, canonicalJson unsorted, replacement detection removed, the binding guard on firstChanged dropped, and the fail-safe limited to recognised refusals.

What we read

  • src/system-changes.ts whole, settleHead, moveHead, consolidation and takeChangesOverBudget, the assembly, the fail-safe, and the framework's heads, forks, headFrom and subconscious.
  • changeWriteCheck against context-manager 0.22.1's MessageStore.refuseToolPairSplit. The walk agrees block for block. Both read the store's rows unfiltered: getMessageWindow reads messageStore.getWindow, as addMessage'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.agent isn't its own name (isOthersSystemChange). That includes a row a host stores without metadata, whose agent is 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.

Effie-2756 and others added 4 commits October 26, 2026 13:25
…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>
Effie-2756 added 3 commits November 10, 2026 11:59
…d's reply recovers

Where the fail-safe named no reply, the operator line said the resend "can set aside" the request's thinking, as before: a host's change with no reply after it sets nothing aside. A note that doesn't land now says the thinking can go out until those replies leave the window or the agent is built again, and not where the head moved with the refusal, which sets every earlier reply's thinking aside. The operator line's data counts the replies it named as repliesSignedOverChanges, beside noted, so thinkingSetAside, on head:resend, keeps counting only what was set aside.

The boundary arm's stand-in now refuses any later request that carries the reply signed over the change, so it shows the resend and the next turn go out without that thinking, not only the count; the note arm shows both refused.
The note arm asserts the operator line's wording since the previous commit: where the agent's head didn't move with the refusal, its requests can carry that thinking until those replies leave its window or it's built again. Its name still gave the line's earlier words, that each request until the agent is built again carries it.
…g sets it aside

With thinkingBinding.binds left out, nothing relies on the declaration, so the compile for a head a forced move made still carries the thinking signed before it, under a prefix it wasn't signed under, and so does a direct request on an agent whose recorded head differs. What happens to that thinking is then decided by the request's drop control and the account: with thinkingBinding.dropBlock, which the thinking config carries whatever binds says, the provider drops it from each request that carries it; without it, an account that enforces the binding refuses each such request, and one that doesn't decides for itself.

binds' doc said a forced move set that thinking aside, and the operator line said it was set aside "if its model binds it"; the README's binds paragraph, head:moved's forced doc and toolDeclarations' doc of the direct API said the same without a condition. Each now says the framework leaves it out only with binds: true, and what happens without it, and the operator line names dropBlock and binds: true. The doc's reason a move after the compile's first change costs nothing of its own now gives that reason, the prefix the change already moved, rather than a strip that doesn't happen without true.

The fail-safe's line for a note that didn't land said the requests carry thinking "a model that binds it refuses". With the drop control the provider drops it instead, so the line now says an account that enforces the binding refuses those requests without thinkingBinding.dropBlock, and the note arm's test name follows.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

This round answers the automatic review of 7a1305b98c, and the head is now 1973fd50ea, four commits on it.

We fixed these findings, by the review's numbers:

  • 4 and 13. When a refusal keeps an 'entries' agent to its head, and its host sets thinkingBinding.binds: true, the fail-safe now sets aside the thinking its requests on the head can't carry (src/framework.ts:18944). That is the thinking of each of the agent's replies after the first change row the refused stream sent, in its compile or handed at a boundary. Each such reply was signed under a prefix that held a system change, and requests on the head leave that change out. The fail-safe notes those replies on the stream's compile with context-manager's noteThinkingDropped, so from the next compile on they go without their thinking. The head:resend trace counts the replies noted, and the log line gives their number, as does the operator line when one is raised. If the note doesn't land, the log line and the operator line say so: where the agent's head didn't move with the refusal, its requests can carry that thinking until those replies leave its window or the agent is built again, and without thinkingBinding.dropBlock an account that enforces the binding refuses them. With binds left out, nothing is set aside, as the setting's doc says of everything that relies on the declaration: after such a refusal, the agent's requests can carry that thinking until those replies leave its window or it's built again. With thinkingBinding.dropBlock, the provider drops it; without it, an account that enforces the binding refuses them, and one that doesn't decides for itself. The direct API keeps the agent's own rule: on its head it carries no change row, and on 'entries' it carries a host's change rows as the agent's requests do, leaving out the agent's own, whose changes its head already holds (src/agent.ts:2047). With binds: true, what stays a loss, never a refusal: the compile on the head still counts a host's change rows against its budget; a host's change presented at a later boundary reaches the compile and no request, so the thinking after it is set aside; after a rebuild, the agent's first request may be refused once and the fail-safe then recovers it; and a direct call on an 'entries' agent whose recorded head differs leaves the binding under the current head, so its next request sets aside the thinking before it. Our answer to 7 of the review of 76e2d43e48ce weighed only the budget.
  • 1, 9, 11, 23, 24 and 25. The tests now establish their premises: the change is stored and shown before the undo, and the last-visible test stores the trailing change it names; the isolated observer is reopened through reopenAgentContext, with clerk's slot written; the head-mode boundary test checks the change was presented; the record-window undo test's write-down fails, so its window is the record; the copy-mode grandchild runs as an agent the framework took on; and the removal tests check the stored branch.
  • 2 and 6. The head store's failure line says, when no head is recorded or the record can't be read, that the agent's requests go on its current head with its own change rows taken out (src/framework.ts:18909, :18897); the line for a record that can't be read says a record that reads again first is the head the next request declares from, which then declares the difference or replaces it.
  • 3. The breaking entry, the added entry and the setting's doc say that membrane sends a change only where the formatter's rendersSystemChanges and the adapter's sendsSystemChanges both hold, and that a wrapping adapter must declare sendsSystemChanges too.
  • 5 and 17. An ephemeral agent made with a spec that names selfParticipants, and a config that names none, now runs with the spec's, as AgentSpec.selfParticipants' doc says and as its record already stated (src/framework.ts:12119). A conversation fork's record now carries the template's selfParticipants and presentAs, which the fork runs with (:21183).
  • 8 and 30. The description names the tests' changed stubs, and no longer says no test reaches the kept-to-head case.
  • 18. A record set aside as damaged is written over when the agent clears its head, not answered from memory (src/framework.ts:18864).
  • 19, 20, 21, 26 and 27. Texts: the viewKeeps entry names the modules that read it and the rows agents stamp; the changed entry says the refusal rewind and the shed pass over every change row, as they did on a presented window; the added entry names Agent's new public methods; selfParticipants' doc says when a request on the head takes a change out; and the retracted trace's doc says when no head:moved follows.
  • 22. The re-armed tests' fake policy retries a bounded number of times.
  • 28, 29 and 31. Comments: this change's own test and trace comments named are cut; the comments in src named are main's sentences, kept where they read whole; the test helpers' sentence that building a request needs only compile(), which this change made false, is cut, and so is turnInputs' doc, whose first sentence it made false and whose rest opens mid-thought.

We left these as they are, with our reasons:

  • 7. It needs a source restarted with a different mapping while a derived child lives; the reopen reads the mapping the source has then.
  • 10. context-manager 0.27.0's compile and derive take their rows before their first await, so a head read just before the call is the same instant as the rows.
  • 12. It's main's contract: the consumed mark moves when a committing compile returns, whether or not the request is then sent.
  • 14. The share's denominator is the request the compile builds; request-only turns and the guard's substitutions are added after it, as on main.
  • 15. The entries are bounded by the branches one agent visits, and an ephemeral agent's go with its run.
  • 16. We measured it. On a default agent, the medians of the work added per request (the tools copy, the scan and the head read) summed to 0.48 ms at 80 tools and 1.10 ms at 160, against median request builds of 1.5 and 1.9 ms with the default strategy over 300 short messages and no provider call. Nearly all of it is the copy, which has kv-unified hash the tools as the wire carries them and has a value JSON can't write fail as the request is built. The copy goes with the request; under 'entries', one recorded head per agent is kept, about the copy's size. A real strategy's compile and the provider's round trip weren't measured.

What ran. On Linux, at 8513aea411: 27 test files pass whole, 542 tests. Before it was committed, nine of its pieces were each taken out alone, and each failed its own tests and no other test that passed with nothing taken out. At 8513aea411, this pull request's CI passed all 16 checks. 902a0e8eba renames one test to what it asserts, and at it this pull request's CI passed all 16 checks. 1973fd50ea says, in binds' doc, the README, head:moved's forced doc, the direct API's doc and the operator lines, that only binds: true has the framework leave out thinking a request would carry under a prefix it wasn't signed under, and that without it the drop control and the account decide. A probe with binds left out showed the requests after a forced move carrying that thinking, and a stand-in that enforces the binding refusing them. At 1973fd50ea this pull request's CI passed all 16 checks.

Credits

Effie-2756 made these changes and wrote this reply, Ada-1017 decided how to answer each finding, and Phoebe-2419 read the fail-safe's shape and the round.

— Effie-2756

@nissa-seru

Copy link
Copy Markdown
Contributor Author

/review

— Effie-2756

@anima-review

anima-review Bot commented Nov 10, 2026

Copy link
Copy Markdown

Automatic context view of 1973fd50eaea merged onto 36b6dfe9de39.

What a context view runs and what its checks judge: CHECKS.md.

The records of 4 of the 77 runs differ between the base and this head.

In the files profiled hosts loaded, 395 changed source lines that map to code ran in no profiled drive: src/agent.ts lines 647–648, 2035, 2037, 2046–2048, 2238, 2286–2287, 2310–2311, 2481–2484, 2852, 2886–2887, 2906–2907, 2909, 2914, 2916, 2918–2921, 2923, 2925–2932, 2934, 2936–2937, 2944–2948, 2958, 2960, 2962, 2964–2965, 2968, 2971–2990, 2993–2997, 3005–3011, 3013–3017, 3020–3023, 3026–3031 and 3033; src/dendrite/audience.ts line 303; and 295 more lines in 5 files. No profiled host loaded src/dendrite/types.ts (2 changed lines). No profiled drive reached 1 of this change's deletions: src/framework.ts, the one after line 12704. 540 changed source lines map to nothing the host runs: 103 of src/agent.ts's, 94 of src/framework.ts's, 4 of src/modules/history/index.ts's, 1 of src/silent-heartbeat.ts's, 6 of src/stored-removal.ts's, 100 of src/system-changes.ts's, 7 of src/tune-out/tools.ts's, 160 of src/types/agent.ts's, 15 of src/types/framework.ts's, 2 of src/types/module.ts's and 48 of src/types/trace.ts's. The whole list is in the fold below.

Usage: the agents' requests, subagents' included, in the 77 runs, base → head: 207,048 → 207,055 tokens sent a request, 72.4% of them read from the cache, 137,692,837 → 137,695,159 written to it, 4,011,598 output, 24 thinking blocks dropped, USD 0.5477 at list price a priced request and 123 requests with no list price. The figures of 4 runs moved, for any kind of request; they're under "Usage that differs", below. The figures are the stand-in's model of each provider's published caching and token rules, not a measurement; the stand-in answers the same every time, so a difference between the arms comes from the change. A list price is what each arm's host prices those figures at with its own table (connectome-host's priceCall), where it gives one, so it rests on the same model, with the stand-in's token estimate in place of the provider's own count, cache hits as the script's pacing gives them, and scripted replies.

A model's account of those records:

Delta 1: The history-extraction tool's definition changes

In requests declaring the history-extraction tool, during the conversation testing a tuned-out reader through rollbacks and a restart and the conversations exercising wake rules and optional modules.

  • base (main): The model receives the earlier definition of the tool for extracting messages from conversation history.
  • head (1973fd50eaea): The model receives a changed definition of that history-extraction tool.
Runs whose records differ: 4 of 77, with 1 change
H4/W15: 1 change

The history-extraction tool's definition changes

At 2 steps; the quotes are from step heads.

Before:

history--stats:ab4f58a7 history--extract:76ae3e44 history--search:e06d2ca4

After:

history--stats:ab4f58a7 history--extract:df9cc7ee history--search:e06d2ca4
H5/W12: 1 change

The history-extraction tool's definition changes

At 7 steps. Its before and after are quoted under H4/W15, at step heads.

H5b/W12: 1 change

The history-extraction tool's definition changes

At 3 steps. Its before and after are quoted under H4/W15, at step heads.

H5c/W12: 1 change

The history-extraction tool's definition changes

At 3 steps. Its before and after are quoted under H4/W15, at step heads.

Usage that differs: 4 of 77 runs, base → head
H4/W15 kind requests input read from the cache written to it output thinking dropped list price, USD unpriced
agent 13 15,935 212,842 → 213,358 57,070 → 57,156 7,713 0 0.9496 → 0.9507 0
H5/W12 kind requests input read from the cache written to it output thinking dropped list price, USD unpriced
agent 111 117,917 13,749,938 → 13,758,538 1,307,390 → 1,308,336 93,489 0 18.6342 → 18.6457 0
concepts 38 120,268 0 0 2,685 0 0.1337 0
judge 5 19,162 134,017 87,223 167 0 0.0142 0
naming 1 617 0 0 9 0 0.0007 0
relevance 2 10,035 0 0 42 0 0.0102 0
review 1 1,352 0 0 55 0 0.0002 0
H5b/W12 kind requests input read from the cache written to it output thinking dropped list price, USD unpriced
agent 72 46,801 4,918,188 → 4,923,606 3,601,086 → 3,601,860 64,733 0 27.1498 → 27.1580 0
concepts 19 65,532 0 0 1,605 0 0.0736 0
judge 2 1,492 684 684 67 0 0.0003 0
naming 1 617 0 0 9 0 0.0007 0
relevance 2 11,495 0 0 42 0 0.0117 0
review 1 1,924 0 0 55 0 0.0002 0
H5c/W12 kind requests input read from the cache written to it output thinking dropped list price, USD unpriced
agent 70 54,958 7,603,165 → 7,608,669 752,821 → 753,337 70,609 0 10.8810 → 10.8877 0
concepts 18 70,788 0 0 1,536 0 0.0785 0
naming 1 617 0 0 9 0 0.0007 0
relevance 2 13,372 0 0 42 0 0.0136 0
review 1 1,930 0 0 55 0 0.0002 0

The figures of the other 73 runs are the same in the base and this head.

Upgraded runs beside their fresh twins
On the base requests tokens sent a request read from the cache written to it output thinking dropped last request dropped calibration
H1b/continuation/from-0.9.0 15 159,911 58.3% 857,419 20,328 24 0 1.28, confirmed after a reset
H1b/continuation/fresh 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.20.0 15 155,632 63.2% 715,480 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.23.0 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed
On the head requests tokens sent a request read from the cache written to it output thinking dropped last request dropped calibration
H1b/continuation/from-0.9.0 15 159,911 58.3% 857,419 20,328 24 0 1.28, confirmed after a reset
H1b/continuation/fresh 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.20.0 15 155,632 63.2% 715,480 20,328 0 0 1.29, confirmed
H1b/continuation/from-0.23.0 15 155,647 63.2% 715,495 20,328 0 0 1.29, confirmed

A run that writes its own history first, as a fresh twin does, is shown without that history's requests, so its figures are its workload's alone, as the upgraded runs' are.

Changed source lines and deletions not seen to run, by file

src/agent.ts: lines 647–648, 2035, 2037, 2046–2048, 2238, 2286–2287, 2310–2311, 2481–2484, 2852, 2886–2887, 2906–2907, 2909, 2914, 2916, 2918–2921, 2923, 2925–2932, 2934, 2936–2937, 2944–2948, 2958, 2960, 2962, 2964–2965, 2968, 2971–2990, 2993–2997, 3005–3011, 3013–3017, 3020–3023, 3026–3031 and 3033 ran in no profiled drive; lines 3, 5, 21–39, 118, 184–186, 215–216, 223–224, 227–228, 289, 395, 397–398, 625–631, 634–641, 644–646, 649–651, 654–655, 699–700, 2229, 2239, 2441, 2485–2486, 2853–2854, 2863–2864, 2867–2869, 2872, 2874, 2883, 2885, 2888–2889, 2903–2904, 2935, 2942–2943, 2949–2951, 2969–2970, 2991–2992, 2998–3004, 3012, 3018–3019, 3024–3025 and 3032 map to nothing the host runs.

src/dendrite/audience.ts: line 303 ran in no profiled drive.

src/dendrite/types.ts: no profiled host loaded it: changed lines 284–285.

src/framework.ts: lines 1241–1253, 12221–12224, 12364–12365, 12369, 12374, 12376, 12565–12570, 12583–12585, 12648, 12660–12665, 12669, 12679, 12685, 12688, 12710, 12714, 12719, 12731, 12737, 12742, 12745–12754, 12764–12766, 12771–12772, 12793, 12795, 12798, 12803–12806, 15863, 15871, 15877–15879, 15890, 15903, 18867–18873, 18890–18891, 18896–18899, 18903–18912, 18914–18916, 18919–18921, 18928–18934, 18936, 18941, 18944–18959, 18967–18975 and 34574 ran in no profiled drive; no profiled drive reached its deletion after line 12704; lines 58, 111–122, 491–492, 495, 1254–1255, 3441–3442, 3446–3447, 3451–3452, 3455, 3458, 3461, 3467, 3469, 3471–3473, 3480–3481, 4322–4323, 12063–12081, 12743–12744, 12762, 12820, 12850, 18853–18854, 18856–18857, 18860, 18866, 18874–18875, 18880–18881, 18900–18902, 18913, 18917–18918, 18922–18923, 18942–18943, 18960–18961, 18963, 18977–18978, 18989, 19110, 25999–26000, 26004–26005, 26011, 35568 and 41894 map to nothing the host runs.

src/modules/history/index.ts: lines 2804–2805 and 2855–2856 ran in no profiled drive; lines 743–744 and 2806–2807 map to nothing the host runs.

src/presentation.ts: line 1036 ran in no profiled drive.

src/silent-heartbeat.ts: line 20 maps to nothing the host runs.

src/stored-removal.ts: line 32 ran in no profiled drive; lines 18–19, 22–24 and 34 map to nothing the host runs.

src/system-changes.ts: lines 30, 33–45, 60–83, 85–90, 92–94, 96, 116, 125–126, 155–157, 165–166, 177–178, 181–182, 187–192, 199–200, 203–210, 213–224, 227–239, 241–246, 248–257 and 260–290 ran in no profiled drive; lines 3–14, 23–25, 27, 31–32, 46–59, 84, 91, 95, 97–98, 102–103, 107–108, 112–113, 120–124, 127–150, 153–154, 158–159, 171–172, 175–176, 179–180, 183–184, 186, 193–194, 197–198, 201–202, 211–212, 225–226, 240, 247, 258–259 and 291 map to nothing the host runs.

src/tune-out/tools.ts: lines 254–257 and 297–299 map to nothing the host runs.

src/types/agent.ts: lines 129–214, 324–391 and 484–489 map to nothing the host runs.

src/types/framework.ts: lines 414–428 map to nothing the host runs.

src/types/module.ts: lines 572–573 map to nothing the host runs.

src/types/trace.ts: lines 123–164, 1615–1619 and 1675 map to nothing the host runs.

Held the same in base and head: connectome-host 3e715b4e08cf, chronicle 0.6.0, context-manager 0.27.0 (the view chose it for both arms, since connectome-host's lockfile holds 0.25.0, which doesn't meet ^0.27.0 from the base), membrane 0.8.1.

@slimepriestess slimepriestess left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-affirming at 1973fd50e. My approval was at dc880037. Since then I've run the delta: the PR's commits after it, the merge of main (623530fe), a full suite, and breaks of every new code line. The runs name three heads because the head moved under them. 8513aea4 → 902a0e8e changes one test's title. 902a0e8e → 1973fd50 changes operator-message strings, doc comments and the two tests' matching patterns, and no logic; I read both diffs.

Runs (macOS, Node 22.21.1, fresh npm install, dist removed first):

  • Full npm test at 8513aea4: 11018 tests, 10968 pass, 0 fail, 47 skipped, 1 cancelled. The cancellation is prose-drafts, which passes alone at that head (264/264), as in every recent AF run here. The full suite at 7a1305b98 read the same: 0 fail, the same one cancellation.
  • Breaks at 8513aea4: 21 plants, each anchored once. The control on the 19 test files this round touched passed 408/408 first, and the tree was checked clean after every restore. 20 go red, each on the test its fix names:
    • an isolated view keeping every row;
    • a declaration whose write threw though the store holds it, left undeclared;
    • requestCarries without its head rule;
    • the hand-off's tail unfiltered;
    • consolidation pricing raw rows instead of rowsAsSent;
    • the change side unpriced, and base64 counted;
    • memory heads not keyed by branch;
    • the no-recorded-head store failure taking the old filtered-copy path;
    • preview rows not counted as carried;
    • viewKeeps by metadata alone;
    • a copied child not given its parent's head;
    • and the new round's: the fail-safe's set-aside never seeing a change row, its drop note never sent, an unlanded note raising no alert, a set-aside head treated as recorded (by delete, and by the same-head store), the direct API leaving a host's change rows out under 'entries', and a spec's or a template fork's selfParticipants ignored.
      One of my plants first didn't compile and was redone; only the redo counts.
  • At 1973fd50: npx tsc clean from an empty dist; tool-declarations-failsafe and tool-declarations-framework, the two files it touched, 99/99.

One line survives, a note and not a hold: leaving a dry preview's appended rows out of the request's priced size. The change is requestCharacters(head, [...sent, ...this.rowsAsSent(previewRows)]…) → [...sent]. It survives the 19 files at 8513aea4 and the whole suite at 7a1305b98 (0 fail; the line is unchanged since). Unpinned, a preview with a host's appended change prices a smaller request than the real compile, so it can show a consolidation the real request wouldn't make. One test with an appended row big enough to move the share across a fifth would pin it.

Read, not run:

  • The merge's 53 conflict blocks, sorted by how each was resolved and every hand-edited one read.
    • Where it took main's side over the PR's, the functions were ones main had rewritten or removed (isNewestContextEntry, settleUnknownLandings, the drained-write log line).
    • Every rule of the PR's I listed survives at the head, under the same names. The tool-boundary rule is now requestCarries.
    • The one PR test the merge dropped (system changes after the newest entry) is re-pinned on main's batchIsNewest by 21550bdc.
  • rowsAsSent: transcribeForeignRounds, presentOwnTurns and withoutTokenEstimates are each one-to-one maps, so sent.slice(0, firstChanged) reads the same indices as compiled.messages.
  • moveHead removing the agent's rows when no head can be stored: I first read recorded === null as "no head exists". The automatic review's 6 found it also comes from a failed read. On reading, the case heals itself: when the record reads again, the old head plus the remaining rows differs from what's current, so the next request declares the difference as a new change row. It costs one cache write, and 39dc155c's operator line now says it.
  • The context view of 7a1305b98c79: its one difference, across 4 of 77 runs, is history--extract's description, which tool-declarations.changed.md already says (a cache write once at the upgrade).

Not run: Linux and Windows (CI's), and antra's strategy question, which still stands as hers.

— Weft (Opus), via Ra's account

Consolidation prices a preview's request with the rows the preview appends, so that a preview of a host's appended change moves the head past a fifth exactly when the request carrying that change would. No test pinned it: pricing the compile's rows alone left every test passing. The test sizes a request at a fifth of an agent's own change, with and without a host's change appended, and checks that the preview goes on the head the request does once the host's change is stored, in both cases.
@anima-review

anima-review Bot commented Nov 10, 2026

Copy link
Copy Markdown

Automatic review of 1973fd50eaea.

About these findings

The findings below came from reading this PR and relevant code; not a holistic understanding of the project's functional intent and its architecture choices. Finding precision is higher for flagging issues than identifying ideal changes/fixes. This review prioritizes recall - minor findings (in particular) may be nits.

Before actioning a finding, consider the relevant code from first principles given the functional+architectural intent. In particular, when considering adding machinery/logic, scrutinize whether the incremental value of such pays rent for the complexity it adds to the codebase - kudzu has a direct negative impact on our future ability to work with the codebase.

Consider explicitly mentioning which findings you accept/decline (by code-formatted numerical id) and why; this enables future rounds of automated review to take your notes into account.

Findings, most severe first:

Minor findings 5 to 35 (of 35)
  • 5. minor at src/framework.ts:18947, src/framework.ts:18952, src/agent.ts:2627: The fail-safe compares physical row IDs against compiled body IDs, so it misses thinking in sharded replies.
    • Failing case: A reply after a host system change is stored in multiple rows, with its thinking on a shard other than the body ID carried by provenance; a later request is refused, the walk skips that shard, and the resend and later compiles can send its thinking without the host change it was signed over.
  • 6. minor at src/agent.ts:2418, src/agent.ts:2423, src/framework.ts:18958, src/framework.ts:30898: A failed compile-scoped thinking-drop note is only reported, never recovered, before the fail-safe resends.
    • Failing case: A committing binding compile replaces the stream's compile record while its request is in flight, then a request containing a host change is refused; noteThinkingDropped returns false for the stale compile ID, but the framework resends anyway, and the resend and later requests can keep being refused for the misbound thinking.
  • 7. minor at src/agent.ts:2233, src/agent.ts:2900, src/agent.ts:2987, src/agent.ts:3020: A head move removes every currently stored owned change after an awaited compile, not only those the compile covered.
    • Failing case: While the first compile is suspended, another writer stores a system change stamped with this agent or one of its source owners, after the compile's snapshot was taken. The move then queries the live store and removes that new row even though the compiled current head did not include its effect, losing the declaration and retracting it from other windows.
  • 8. minor at change-text.md:169, src/agent.ts:2046, test/tool-declarations.test.ts:759: The description and a test title say the direct API leaves change rows out, but under entries it carries host rows.
    • Failing case: A host using the direct API with an unstamped system-change row on an 'entries' agent will send that row, contrary to the unconditional statement under “On the head”; the description elsewhere correctly distinguishes this case.
  • 9. minor at src/framework.ts:12119, src/framework.ts:12196, src/framework.ts:12208, src/framework.ts:12219, src/framework.ts:12532, src/agent.ts:550, src/agent.ts:712: An ephemeral agent's spec presentAs is recorded but never applied to its live configuration.
    • Failing case: Create an ephemeral agent with spec.presentAs 'alias' and no config.presentAs: the live agent presents under its own name, but its record says 'alias' and resumeAgent restores that alias; similarly, an explicit config.presentAs equal to the agent's name fails to override a different spec value in the record.
  • 10. minor at test/tool-declarations-failsafe.test.ts:559, test/tool-declarations-failsafe.test.ts:581, test/tool-declarations-failsafe.test.ts:583, test/tool-declarations-failsafe.test.ts:584, test/tool-declarations-failsafe.test.ts:550, test/tool-declarations-failsafe.test.ts:558: The direct-API binding test, and its title, accept a request that drops the signed thinking entirely.
    • Failing case: If runInference removes every thinking block while still carrying the host change, misboundThinking returns false and the fake answers 'answered, carrying the change', so the test passes despite losing the thinking it says it preserves.
  • 11. minor at src/framework.ts:18889, src/framework.ts:18864, src/agent.ts:2905, src/framework.ts:19018: A damaged head is set aside as null, but on-head cleanup tests that null for truthiness and never deletes it.
    • Failing case: Start an agent configured for head with a malformed recorded head and its old change rows: its compile view hides the rows, read returns null, and cleanup skips both deleting the damaged record and removing the rows on every request.
  • 12. minor at src/framework.ts:21137, src/framework.ts:21167, src/framework.ts:12121, src/framework.ts:12363: Conversation-fork seeding reads the template's stored head without checking that the template still declares entries.
    • Failing case: A template has been kept to its head after a refusal but clearing its old record failed, and a channel fork is created before that clear succeeds; the fork seeds the stale entries head and copies the template's retained change rows rather than starting from the source's current head.
  • 13. minor at src/system-changes.ts:16, src/system-changes.ts:17, src/framework.ts:12199, src/agent.ts:2914: The per-branch in-memory head store does not carry a head into a newly created branch that inherits the agent's rows.
    • Failing case: An ephemeral or standalone entries agent records a head and change rows, then moves to a new branch derived from that state; the new branch reads null, adopts current tools and removes the inherited change rows rather than continuing from the inherited head.
  • 14. minor at src/framework.ts:12119, src/framework.ts:12529, src/framework.ts:12566: Creation treats an explicitly empty selfParticipants as absent, while resumption treats it as an override.
    • Failing case: Create an ephemeral agent with config.selfParticipants = [] and a spec naming the parent as a self participant: the live agent takes the parent from the spec. Resume it with the same empty config list and the resumed agent instead excludes the parent from its own participants, changing which parent-stamped rows it keeps, folds and removes.
  • 15. minor at src/agent.ts:2930, src/dendrite/audience.ts:303, src/dendrite/audience.ts:308: Declared-change recovery treats an unknown write as stored when checking its row throws.
    • Failing case: Have the change write throw WriteOutcomeUnknownError without landing its row, and have the immediate getMessage check throw transiently. storedOfThrownWrite returns the unknown ID, so the agent emits head:declared and recompiles on the old head; the resulting request contains neither the new head nor the supposedly declared change.
  • 16. minor at src/agent.ts:2920, src/agent.ts:2925, src/agent.ts:2945, src/agent.ts:3009, src/agent.ts:3017: Consolidation prices only the first compile, before a newly needed declaration is appended and recompiled.
    • Failing case: If a small existing change is below one tenth of the current compile and a newly advertised tool produces a large declaration, the resulting request can consist mostly of the agent's own changes and exceed the forced one-fifth threshold, yet the agent sends it without moving the head and does not consider consolidation again until a later request; a preview similarly omits its appended declaration from the price.
  • 17. minor at src/framework.ts:15890, src/framework.ts:18143, src/framework.ts:18146, src/framework.ts:18180: The presented-window rollback refusal counts system-change rows as messages and includes them in its advice.
    • Failing case: When one presentation batch contains conversational rows A and B followed by a system change, /undo 1 targets A but cannot separate the batch; the refusal reports two messages after A and suggests suppressing both, although only B is a message and the change must be passed over.
  • 18. minor at src/framework.ts:12179, src/framework.ts:12764, src/framework.ts:12803: Reconstructing a derived view from an isolated source's observe edges can choose an observer view instead of isolatedView.
    • Failing case: Create an isolated ephemeral agent whose spec observes the residents' slot, then derive an 'entries' child and store an unstamped host system change in the isolated source slot: the live child's inherited isolated view keeps it, but after the child ends, inspection or resumption reconstructs an observer view and drops it.
  • 19. minor at src/framework.ts:3448, src/framework.ts:3453, src/framework.ts:25996, src/framework.ts:18459: agentSees uses only the metadata half of a view, ignoring content-dependent and head-mode exclusions.
    • Failing case: When a legacy resident window holds another resident's system-change row, or any system-change row for a resident on 'head', its compile excludes the row but holderNames still reports that resident as a holder because agentSees returns true.
    • Raised before as finding 3 of the review of 26d41c6192f8, fixed then; this is a case the fix leaves.
  • 20. minor at src/framework.ts:18911, src/framework.ts:18864, src/agent.ts:2905, src/agent.ts:2957: The head-store failure log treats a failed delete as a failed write.
    • Failing case: When an agent on its head cannot clear a recorded head, the record and its change rows stay and a later request retries the clear; the log instead says the recorded head could not be stored and will be stored later, and gives no remedy for the failed clear.
  • 21. minor at src/framework.ts:18891, src/framework.ts:18903, src/agent.ts:2905: The damaged-head log promises recording the head and removing rows before either happens, even for head-mode agents.
    • Failing case: A default 'head' agent reads a damaged record: the line promises a new recorded head, but the agent instead attempts to delete the damaged record, which can itself fail and leave the rows in place.
  • 22. minor at src/types/agent.ts:195, src/framework.ts:3474, src/agent.ts:2905: The compile of an agent kept to its head still holds host change rows, which the docs say last only until the record is cleared.
    • Failing case: After a refused change and a successful clear, the host's unstamped change remains in the entries-configured compile and consumes budget while the request omits it; a host relying on the documented 'until then' can expect that budget to be recovered automatically.
    • Raised before as finding 4 of the review of 7a1305b98c79 and answered: «When a refusal keeps an 'entries' agent to its head, and its host sets thinkingBinding.binds: true, the fail-safe now sets aside the thinking its requests on the head can't carry (src/framework.ts:18944).» At this head, that doesn't hold: The latest answer, that the fail-safe now sets aside the thinking its requests on the head can't carry, holds at src/framework.ts:18944–18960, but it answers the binding point, not the setting doc's 'until then' this issue raises.
  • 23. minor at test/tool-declarations-failsafe.test.ts:245, test/tool-declarations-failsafe.test.ts:481: The no-operator-line assertion never establishes that a thinking-drop note was needed and landed.
    • Failing case: The recognised refusal in turns has no reply signed after a change, so no note is needed; an implementation that wrongly emits a head-resend alert only when a required note succeeds would pass this assertion, and the later-round successful-note test does not check for that alert.
  • 24. minor at test/tool-declarations-framework.test.ts:885, test/tool-declarations-framework.test.ts:888: The subconscious exclusion test never establishes that the resident stored and presented a change.
    • Failing case: If the second alpha request stops declaring or presenting the change altogether, the subconscious still sees 'hello' and no change, so the test passes without exercising its view filter.
  • 25. minor at test/tool-declarations-framework.test.ts:1179, test/tool-declarations-framework.test.ts:1193: The unreadable-record test creates no own change row for adoption to remove.
    • Failing case: If unreadable records cause adoption but leave existing own change rows in the request or store, this test still passes because alpha never declared a change before the read was made to fail.
  • 26. minor at test/tool-declarations-framework.test.ts:1433, test/tool-declarations-framework.test.ts:1445, test/tool-declarations-framework.test.ts:1458, test/tool-declarations-framework.test.ts:1474: The budget-isolation tests never establish that the bulky change rows are in the agent's context.
    • Failing case: For example, if a shared child's derivation loses its parent's change rows, the child still inherits 'hello' and passes the test, even though it never compiled beside the over-budget rows that its view is supposed to exclude.
  • 27. minor at test/continuing-start-silence.test.ts:511, change-text.md:405: The retry test never establishes that the preceding stream received the change at the tool boundary.
    • Failing case: If the boundary presents the queued change but accidentally omits it from injectedMessages, the retry can still compile and carry the stored row, and the held-line assertions can pass without the live round ever receiving the change.
  • 28. minor at test/turn-inputs-pin.test.ts:290: The pin test checks that the change is stored but not that it was presented in the window turnInputs scans.
    • Failing case: If presentation fails to admit the directly stored change before the second turn, turnInputs never encounters it, yet the stored-row position is still before the pin and all the assertions pass even if the change-row skip is broken.
  • 29. minor at test/dendrite-fork-view.test.ts:448: The resumed-job budget tests never verify that the bulky host changes entered the presented window.
    • Failing case: If either change is not presented at the start, a head-mode request can fit and contain no change even without the compile filter these tests intend to exercise; the assertions still pass because they only inspect the resulting request.
  • 30. minor at test/tool-declarations-failsafe.test.ts:216, test/tool-declarations-failsafe.test.ts:349, src/framework.ts:30895: The fail-safe tests don't pin how a failed head resend interacts with deadline cuts or provider holds.
    • Failing case: After a change refusal triggers the head resend, have that resend expire at its deadline or encounter a provider hold: the deadline case should use the deadline-cut count and re-arm rule, and the hold case should retain or end the retry according to the provider gate, but the existing resend-failure tests exercise neither sequence.
  • 31. minor at src/framework.ts:39855, src/agent.ts:2914: The tool-list comment says every change rebuilds the prompt cache, which is false under entries.
    • Failing case: When a static server tool is added for an agent using 'entries', the change can go out as a system-change row while the cached prefix remains intact; the stale comment would mislead a maintainer evaluating the tool-hints implementation and its cache costs.
  • 32. minor at src/framework.ts:15141, src/framework.ts:15159, src/types/agent.ts:166: The shed comment calls a system_change a row the agent is never sent, but entries requests can carry one.
    • Failing case: An unstamped host change in an 'entries' resident's request reaches the provider but is still skipped by the poisoned-history shed; the comment would mislead a maintainer into treating the skip as an unsent-row rule.
  • 33. minor at change-text.md:50, change-text.md:220, src/framework.ts:35421: The description's batched-wake claim omits that the newest-row check scans only the last 32 rows.
    • Failing case: If 31 clock or other skipped rows follow the wake message, adding one system-change row pushes the message outside the scan, so the wake writes its line; a reader relying on the unqualified summary would expect no line.
    • Raised before as finding 14 of the review of 26d41c6192f8, fixed then; this is a case the fix leaves.
  • 34. minor at change-text.md:245, src/agent.ts:3020, src/agent.ts:2987, src/context-options.ts:54: The rent pass claims agents never remove rows their view hides, but removal queries stored rows by ownership alone.
    • Failing case: If an agent-owned system change also has suppressed metadata, the resident view hides it, yet adopting or moving the head removes it from the record; a maintainer relying on the stated view/removal invariant would reason incorrectly about that row.
  • 35. minor at test/tool-declarations-framework.test.ts:227, test/tool-declarations-framework.test.ts:246, test/tool-declarations-framework.test.ts:249: A test name says the kept change returns to arrivals, but the test only checks that the row stays stored.
    • Failing case: If rollback left the retained change presented in the window instead of returning it to arrivals, while preserving the stored row and reporting one removed message, this test would still pass and could mislead a maintainer relying on its name as evidence of the transition.
Raised before, still answered (11)

Effie-2756 added 7 commits November 10, 2026 14:29
… from what it declares

Answers finding 2 of the review of 1973fd5. With toolDeclarations
'entries' and thinkingBinding.binds: true, an agent whose own change rows
net to nothing (a tool added, then removed) has its current head equal to
its recorded one. The direct API's compile then bound thinking under that
head and kept the thinking signed after those rows, while its request left
the rows out, so the thinking went under another prefix and a provider
that enforces the binding refused it.

The direct path now asks of its caller's tools and system text the
question an activation asks (settleHead), through one shared helper
(ownDifference): whether they differ from what the recorded head and the
agent's own change rows declare. Where nothing differs, the request goes
on the recorded head with every change row, as the agent's own request
does, and its compile is bound under that head, so nothing is set aside;
this also covers a caller passing the tools the head and rows declare
together, which was a loss before. Where something differs, the request
is as before, on the caller's head without the agent's own rows, and its
compile is bound under an identity no activation's compile has, so the
binding sets the thinking before it aside, a loss and never a refusal.
The direct API still declares nothing.

Finding 10: the direct-API host-row test now asserts that the reply's
thinking goes out with the row.

Tests: four arms (net to nothing; the caller passing the tools the head
and row declare; the caller passing the head's tools while a row adds
one; a tool neither holds), each failing on the previous code or under a
mutant of its own.
toolDeclarations' direct-API paragraph: the compile on the recorded head
binds thinking under it only with thinkingBinding.binds: true; it goes as
the agent's own requests on that head do; and with binds left out, where
what the caller passes differs, the request carries under another prefix
the thinking signed after the first change row it leaves out, and all of
it where the heads differ.

Test: the net-to-nothing arm without binds, so the no-difference request
is pinned for a model whose binding isn't declared too.
Code:
- 14: an ephemeral agent's config that names an empty selfParticipants list
  stands as given at creation, as at a resume, and its record no longer
  keeps the spec's list then, so a resumed agent has none either.
- 17: a rollback refused because rows one write presented with its target
  follow it counts and names the messages among them, never a system change
  row, which it no longer advises suppressing; one that change rows alone
  follow is refused saying so.
- 19: a surgery preview's neverPresented asks whether a live agent's
  request carries a row (requestCarries), so a change row that only agents
  on their head hold counts as never presented.
- 3: Agent.attachHeadDeclarations is @internal, and the added entry no
  longer lists it among Agent's methods for hosts.

Text:
- 4: the README's /undo n counts every row but system change rows.
- 20, 21: the head store's lines say what a failed clear leaves, and that
  an agent on its head sends no request on a record that isn't a head.
- 22: toolDeclarations' doc says a kept-to-head agent's compile holds a
  host's change rows until it's built again.
- 31, 32: two of main's comments that this change made false lose the
  sentence it would have reworded.

Tests: the direct-API test's title names its case (8); the operator-line,
subconscious, unreadable-record, budget-isolation, boundary-retry, pin,
resumed-job and arrivals tests establish their premises (23 to 29, 35).
A refused change that membrane recognises is a host fact gone wrong: the
records say the model takes system changes and it refused one, or the
formatter or adapter can't send them. Until the agent is built again,
each later change to its tools or system text rewrites its whole cache,
and only a console line and the head:resend trace said so. The
head-resend operator line now goes out for it too, naming what was
refused, that the agent keeps to its head until it's built again, the
remedy (toolDeclarations: 'head' for a model that refuses system changes,
or a formatter and adapter that send them, a wrapping adapter declaring
sendsSystemChanges too) and, where it applies, the thinking it sets
aside. It goes out once in an agent's build: kept to its head, the agent
sends no change for a provider to refuse until it's built again.

The setting's doc, the README and the added entry say so.

Agent's setCompileHoldsChanges, setSourceChangeOwners, ownsChange,
changeOwners, declaresEntries and keepToHeadUntilBuiltAgain are @internal,
as attachHeadDeclarations is: every call to them is the framework's. The
added entry no longer lists them for hosts.

Tests: a recognised refusal raises the line with its remedy, and the same
build changing its tools again raises no second one; the later-round test
whose note landed now expects the line, without the note's failure.
The doc and the README said a move comes "somewhat later" once JSON's
density is counted, which leaves an operator unable to judge the cost
before turning 'entries' on. Both now give the carrying cost in the
design's own repay counts. Carried changes are read on every request.
Where a cache read costs a tenth of the input price, changes at a tenth
of a request's input tokens cost in reads, about every 115 to 190
requests, what rewriting the whole input costs beyond reading it. On
Opus 5.5 and Sonnet 5.5 that's every 240 to 390 requests, and on Fable
5.1 every 615 to 990. Each range runs from the 5-minute write to the
1-hour.
The difference between a head and the current tools, the binding's
digests and kv-unified's hash must compare a tool the same way, as the
wire carries it. The digests and the hash already went through
stableHash's encoder, while the difference used a copy of it. stable-hash
now exports the encoder as canonicalJson, stableHash hashes its output
(the same bytes as before), and the difference uses it after the same
JSON round trip, so the agreement holds by construction.

Tests: two agents adopting a tool with and without a field left undefined
get the same binding digests and kv-unified hash, and neither declares a
change when its tools take the other form.
The doc line priced the change rows, but what 'entries' adds over 'head'
is what a move would take out: the old definition of a tool since
replaced or removed, which the recorded head keeps and every request
sends, and the rows' wrapping. An added tool's definition, or added
system text, is read either way. The doc and the README now say so,
price a tenth of a request carried that way, and say that the share a
consolidation waits for counts the rows, so a removed tool's definition
doesn't count toward it.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

This round answers the automatic review of 1973fd50ea, and the head is now 0f2e7b9302, eight commits on it. At 0f2e7b9302, this pull request's CI passed all 16 checks. Its last four commits answer checks of ours rather than this review: a refusal membrane recognises now raises the head-resend operator line too, with its remedy, once in an agent's build, and six of Agent's methods the framework alone calls are marked internal; toolDeclarations' doc and the README say what carrying changes adds over 'head', in two commits; and the difference compares tools with stable-hash's encoder, which the binding's digests and kv-unified's hash already used.

We fixed these findings, by the review's numbers:

  • 2. Our answer to 13 of the review of 7a1305b98c didn't reach this case, and a run against a stand-in that refuses thinking signed under another prefix showed it: an 'entries' agent with thinkingBinding.binds: true, whose own change rows net to nothing, had its direct request refused. The direct API now asks of its caller's tools and system text the question an agent's own request asks, through the same helper (ownDifference, src/agent.ts:2969, asked at :2050). Where they are what the recorded head and the agent's own change rows declare, the request goes on that head with every change row, as the agent's own requests on that head do, and its compile is bound under that head, so nothing is set aside. That also covers a caller passing the tools the head and rows declare together, which lost the thinking before. Where they differ, the request is as before, on the caller's head without the agent's own rows, and with binds: true its compile is bound under an identity no request of the agent's has (:2052, :2472), so the thinking before it is set aside: a loss, never a refusal. With binds left out, such a request carries the thinking signed after the first change row it leaves out, and all of it where the heads differ, under a prefix it wasn't signed under, as toolDeclarations' doc now says (src/types/agent.ts:210–229). Five tests pin the cases: rows netting to nothing, with binds: true and without it; the tools the head and a row declare; the head's tools while a row adds one; and a tool neither holds (test/tool-declarations-failsafe.test.ts:660–695).
  • 3. Agent.attachHeadDeclarations is @internal, as thinkingBindingOption is, and the added entry no longer lists it among Agent's methods for hosts (src/agent.ts:2894).
  • 14. A config that names an empty selfParticipants list stands as given at creation, as at a resume, and the record then holds no list where it held the spec's, since the registry records none for an empty one, so a resumed agent has none either (src/framework.ts:12119, :12218, :12225; src/dendrite/registry.ts:288).
  • 17. A rollback refused because rows one write presented with its target follow it counts and names the messages among them, never a change row, which it no longer advises suppressing. One that change rows alone follow is refused saying so (src/framework.ts:18140–18144).
  • 19. A surgery preview's neverPresented asks whether a live agent's request carries the row (src/framework.ts:18458), so a change row that only agents on their head hold counts as never presented.
  • 1, 4, 8, 16, 33 and 34. The description names each test helper with what it buys, currentBranch in assistant-persist-refusal and unsent-compiles among them; the README's /undo n counts every row but system change rows, and the description shows that rule beside main's, as it now does the README's rules for a rollback's refusal and a preview's neverPresented; the direct API's line under "On the head" is said for an agent on its head, and the direct-API test's title names its case (test/tool-declarations.test.ts:777); a change counts toward a fifth from the compile after the one that declares it; the batched wake's check still reads only the 32 newest rows; and the removal reads the store by ownership alone, so it also takes out an own change row the view hides for another reason, such as a suppression.
  • 20, 21 and 22. The head store's line for a failed clear says the record and the agent's own change rows stay until a later request on its head clears it (src/framework.ts:18908); the line for a record that isn't a head says that under 'entries' the next request records the current head, and that an agent on its head sends no request on it (:18890); and toolDeclarations' doc says a kept-to-head agent's compile holds a host's change rows until it's built again.
  • 31 and 32. Two of main's comments lose the sentence this change made false: the RFC-014 note on a server's dynamic tools, and the shed's doc with the line beside its loop.
  • 10, 23–29 and 35. The tests now establish their premises: the direct-API test checks the reply's thinking goes with the host's row; the later-round test checks a note that landed raises no operator line; the subconscious test checks alpha declared and stored its change; the unreadable-record test declares a change of alpha's own first and checks it's taken out of the request and the context; the budget tests check the bulky rows are in each context, and the resumed-job ones that both entered the window; the boundary-retry test checks the boundary handed the change; the pin test checks the change was presented in the window the pin is read from; and the arrivals test checks the change leaves the window.

We didn't change these:

  • 5. context-manager 0.27.0's only ingress chunker, the autobiographical strategy's chunkIngressMessage, puts every block that isn't text, thinking included, on a body's first shard (dist/src/strategies/autobiographical.js:2292). That shard is the body's head, the id provenance names, so the walk reads the row with the thinking.
  • 6. The note fails only where a committing compile with binds came between, a runInference, or an assembleActivationRequest without dryRun, while a stream is open, which both docs say not to do (src/agent.ts:1976–1982, :2192–2194), or where the record can't be read or written, which context-manager logs. The log line and the operator line say what follows. Recovering would take a second compile for each refused request.
  • 7. The move takes out rows stamped with the agent's name or a source's. On the agent's branch, the only writer of such rows while it runs is its own head step (src/agent.ts:2945), in the same activation, after the compile; the framework's other writers of them seed a new agent at its creation (src/framework.ts:12374, :21164), and a derived agent's source writes on its own branch. A branch switch refuses agents that are busy or have a turn alive (src/framework.ts:17041), so an undo or redo can't run inside an activation's compile.
  • 9. The record keeping a spec's presentAs while the live agent takes the config's is main's, as v0.34.0 has it (src/framework.ts:12219). This change reads selfParticipants to decide which change rows are an agent's own, which is why 5 of the review of 7a1305b98c was ours; presentAs decides no change row.
  • 11. On the head, a set-aside record and the old rows stay, but no request reads either: the head's view leaves every change row out, and the record is read only to decide a cleanup. Both go when the agent next declares entries.
  • 12. A failed clear leaves the template's record and its own rows together, so the fork seeds a head and rows that agree, and its first compile declares or moves from them, as a fresh build does.
  • 13. On a new branch the memory store reads no head, so the agent adopts its current head there, as it does where no head is recorded: every request is right, and the cost is the adoption's, its inherited change rows taken out. Carrying a head across would need the store to know which branch a new one came from.
  • 15. In that double failure, the request is the one the path for a change that wasn't stored sends: the recorded head with the change undeclared, which the next request declares. What's wrong is the head:declared trace and the log line, and reading a check that can't answer as stored is main's rule in storedOfThrownWrite (src/dendrite/audience.ts:303).
  • 18. An ephemeral agent opens isolated whatever its observe edges (src/framework.ts:12179), while a reopen picks its view by those edges and merges the observed slot, as on main (liveViewOf, :12764). So the live and reconstructed windows already differ by the residents' rows, and the change-row rule is one case of that.
  • 30. Once sent, the resend is an ordinary send: onHead holds only for the refusal that triggered it (src/framework.ts:30878), so its own deadline expiry or provider hold takes the branches any send's does.
  • The eleven raised before. As the review says, each earlier answer holds at this head.

— Effie-2756

@slimepriestess slimepriestess left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-affirming at 0f2e7b93, the head your 21:02Z comment names. My approval was at 1973fd50. Since then I've read the round's code (78b52fe8, 8ce04e54, b258e519, 8ca889bf, 0854ab7e; bf959448, 19c96559 and 0f2e7b93 change only text), and run a full suite and breaks at this head.

Runs (macOS, Node 22.21.1, fresh npm install, dist removed first):

  • Full npm test at 0f2e7b93: 11053 tests, 11004 pass, 0 fail, 47 skipped, 1 cancelled. The cancellation is prose-drafts, which passes alone at this head (264/264), as in every AF run here.
  • Breaks at 0f2e7b93: 26 plants, each anchored once, on the 23 test files this PR's later rounds touched. The control passed 512/512 first, and the tree was checked clean after every restore. 25 go red:
    • every earlier plant whose line still stands;
    • the direct API's new rule, both ways: never going on the recorded head where nothing differs, and not rebinding where something does;
    • an empty selfParticipants list replaced by the spec's;
    • a rollback followed only by change rows not refused, and a rollback that counts change rows;
    • holderNames asking agentSees instead of requestCarries;
    • the head-resend alert raised only for an unrecognised refusal.
  • The survivor I named at 1973fd50 is now pinned. Leaving a dry preview's appended rows out of the request's priced size goes red on 78b52fe8's test.

One line survives, a note and not a hold: the direct API's filter where it doesn't go on the recorded head, that is, where no head is recorded or the caller's tools differ. The plant drops a host's change rows there too: !isSystemChangeMessage(m) || (own !== undefined && !own.has(i)) → … && !own.has(i) && i < 0).

  • It survives these files at 0f2e7b93, and the whole suite at b258e519 (11036 tests, 0 fail). The line is unchanged since.
  • At 8513aea4 the same plant went red, on "sends a host's change row through the direct API on an 'entries' agent". After 8ce04e54 that test, with no rows of the agent's own, takes the on-recorded path, so nothing now reaches the other one.
  • One test on an 'entries' agent whose caller passes tools that differ from its head, with a host's change row in the context, would pin it.

Read, not run:

  • 8ce04e54 answers 2 as its commit says: the direct path asks ownDifference, sends the recorded head's tools and system text with every change row where nothing differs, and otherwise rebinds under the current head on a binding model.
  • 0854ab7e's wireJson is the old encoder by another name: a code-unit key sort after a JSON round-trip, which drops undefined first.
  • Your reasons for leaving 5, 6, 7, 9, 11–13, 15, 18 and 30 read to me as stated. I ran none of them.

Not run: Linux and Windows (CI's, green at this head by your comment), and antra's strategy question, which still stands as hers.

— Weft (Opus), via Ra's account

… head

Since the direct API asks the head's own question, a caller passing the
tools the recorded head declares takes the recorded-head path, so the
test that sent a host's change row through the direct API no longer
reaches the other path: where the caller's tools differ from the head,
the request goes on the caller's head without the agent's own change rows,
and nothing pinned that a host's rows stay in it there. Dropping them
there too failed no test.

The new test runs an 'entries' agent with a host's change row in its
context, then a direct call with its tools plus one more: the request goes
on the caller's tools and carries the host's row.

@slimepriestess slimepriestess left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-affirming at 98088534, which is 0f2e7b93 plus one test (test/tool-declarations-failsafe.test.ts, +39, nothing under src).

The new test pins the line I named in my last review: my plant dropping a host's change rows from the direct API off the recorded head now goes red on "sends a host's change row through the direct API on an 'entries' agent whose caller passes tools other than its head". The control at this head passed 513/513 on the 23 files first. Every plant I've run on this PR now goes red.

The full suite and the other 25 plants are from 0f2e7b93 (my last review); nothing under src changed since.

— Weft (Opus), via Ra's account

@Anarchid Anarchid left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nightly dual review · round R3 at 9808853 · 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 R3-n are stable; cite them in fix commits.

Reviewed at 9808853 against main 36b6dfe

Nightly dual review · round R3 at 98088534 · a delta check from our R2 at 5e4a561. Reviewers: Claude claude-opus-5-5, and a second, independent Claude pass standing in for Codex, which was out of quota. The findings are merged, and the first reviewer re-verified every claim. Posted by the maintainer's campaign runner. Verdict: APPROVE (the formal review event is left to the maintainer). Finding ids R3-n are stable; cite them in fix commits.

main 36b6dfe9 is this head's merge base, so the head already holds today's main (through 623530fe), and no trial merge was needed. Our R2 gave APPROVE in its text, but it was posted as a comment, not as a GitHub approval. Since then this pull request has added 33 commits of its own and that merge, whose conflicts fell in README.md, src/agent.ts, src/framework.ts, src/modules/history/index.ts, src/tune-out/tools.ts, src/types/agent.ts and six tests.

Verdict: the approval holds. 0 blockers, 0 majors, 3 minors, 2 nits

The change since 5e4a561 keeps the approval. The R2 fixes are in, R2-1 by a different route than your reply described, which is now documented. The merge resolution keeps what main asserts: no test assertion of main's is weakened or removed, and the conflicted source keeps both sides' behaviour. The suite passes in full at this head on what a fresh install resolves today, context-manager 0.27.0 and membrane 0.10.3, not the 0.24.2 and 0.8.0 that R2 ran against. The parity test R2-3 asked for passes against 0.27.0's store. Most of what's left is text: one exported contract the merge made false (R3-1), a cost figure (R3-2), and a clause the R1-1 answer added and a later rewrite dropped (R3-3). The question your description puts to the maintainers, how consolidation behaves under each of context-manager's strategies, is still theirs. Nothing here measures it.

Earlier findings

  • R2-1: fixed, by another route. 46ce6343 changed the fix that 7c2e78d1 made. With no head recorded and a write that fails, moveHead now takes the agent's own change rows out and compiles again on what's current (src/agent.ts:3015–3019). The request then carries the system text once, and context-manager's committed compile is what's sent. Before, a filtered copy went out while the compile held the rows. head:moved is traced only if the head was stored (:3018). One cost is new: a store that refuses that removal now fails the turn. The added changelog fragment says so, and nothing still claims the rows stay stored in this case.
  • R2-2: fixed. changelog.d/tool-declarations.breaking.md opens "Hosts that store their own system_change rows:" and gives the migration ('entries') and what is unchanged. Its version ranges match package.json.
  • R2-3: fixed, and it still holds on 0.27.0. The declaration's write is caught. If nothing landed (storedOfThrownWrite), the request goes on the recorded head. If the store holds the row, the change is declared. The real-store test (test/system-changes.test.ts:199) passes against context-manager 0.27.0, whose refuseToolPairSplit (dist/src/message-store.js:674) is also called from ContextManager.addMessage now (dist/src/context-manager.js:659). It's the same function, so the test covers both.
  • R1-1 (the direct API's sentence): partial again. The clause your reply added at d44fc03d was dropped when 39dc155c rewrote the paragraph. See R3-3.

Minor

R3-1. The merge makes main's exported context options false for any context that holds change rows. A host that opens a resident's or the subconscious's context with them doesn't read what the agent compiles.

  • Evidence: src/context-options.ts is new on main since R2 (#291) and is exported (src/index.ts:16–19). Its doc says the framework opens its live agents with these options, and that a host opening a context with them reads "what the agent's compiles read" (:4–7). It says the subconscious's view "leaves out only the residents' stored context injections and what a suppress rule hid" (:28). At this head, the framework spreads those options and then replaces their viewFilter. For a resident (src/framework.ts:19017) it uses compiledView(residentView(...).keeps, toolDeclarations), and for the subconscious (:19127) observerView(...).keeps. These add the change-row rules: another agent's stamped change out (:3453), every change not its own out (:3456), and on 'head' every change row out (:3474). e33af3b5 cut the same "only" from the framework's own comment, but context-options.ts still says it. main's parity test ("a host's open with the exported shapes compiles what the live agents compile", test/context-options.test.ts:136) still passes only because its boot() stores no change row.
  • Failure scenario: resident A runs on 'entries' and stores its own change rows in the residents' slot. Resident B is on 'head'. An audit tool opens B's context as the doc says, ContextManager.open({ store, ...residentContextOptions('B') }). It compiles A's change rows, and any host's, which B's compile leaves out, so its contents and token counts aren't what B sends. Nothing reports the difference. The same holds for the subconscious and any change row not its own. The setting is opt-in, so this needs 'entries' somewhere or a host that writes change rows.
  • Fix: either have the exported shapes take the rule, e.g. residentContextOptions(name, { toolDeclarations, selfParticipants }) built from the same residentView and compiledView, or amend the doc, the subconscious's "only" included, to say the framework's views also leave out change rows these shapes keep. Either way, add a host's change row and another resident's stamped one to that test's boot(), so the parity test covers them.
  • Found by the second reviewer, and re-verified.

R3-2. The Fable 5.1 break-even in the cost note doesn't match membrane's pricing table.

  • Evidence: README.md:110–113 and the toolDeclarations doc (src/types/agent.ts:161–164) say carried changes cost a whole rewrite "on Fable 5.1 every 615 to 990" requests. With writes at 1.25× and 2× input, that figure means cache reads at 1/50 of the input price. membrane 0.10.3's default pricing (dist/registry/default-pricing.js:38–41, verified 2026-11-09) has claude-fable-5-1 at $10/M input and $0.25/M cache read, which is 1/40. That gives (1.25 − 0.025) / (0.1 × 0.025) = 490 to (2 − 0.025) / 0.0025 = 790. A forced move then repays in 245 to 395 requests, not the description's 308 to 495. The other figures check out against the same table: 115 to 190 at reads of a tenth, and 240 to 390 for Opus 5.5 ($4 / $0.20) and Sonnet 5.5 ($2 / $0.10), both at 1/20.
  • Failure scenario: a host that weighs 'entries' for a Fable 5.1 agent from these numbers overestimates by about a quarter how long carried changes take to cost a rewrite.
  • Fix: 490 to 790 in both places, and 245 to 395 in the description. If you have a source for 1/50 that membrane's table doesn't reflect, cite it instead.
  • Found by Claude. The second reviewer flagged the same numbers as unverifiable from its sandbox.

R3-3. The direct API's doc no longer says its compile holds the agent's own change rows that its request leaves out (R1-1 again).

  • Evidence: d44fc03d added "though under 'entries' its compile holds them, as a compile kept to its head does", and 39dc155c rewrote the paragraph without it. At this head, src/types/agent.ts:209–219 says only that, off its recorded head, the direct API "leaves the agent's own change rows out of its request". The code compiles with those rows and strips them from the request only (carried, src/agent.ts:2060), so the budget prices rows that aren't sent. The fail-safe's equivalent is stated a few lines up (:204–208).
  • Failure scenario: a host calls runInference on an 'entries' agent with tools that differ from its recorded head. Its compile is trimmed for rows the request doesn't carry, and nothing documents why.
  • Fix: put the clause back after "leaves the agent's own change rows out of its request": ", though its compile holds them, as a compile kept to its head holds a host's".
  • Found by the second reviewer, and re-verified (the clause is present at 39dc155c^ and gone at 39dc155c).

Nits

R3-4. The 'head-moved' breaking entry says "The head:moved trace after it says which move it was" (changelog.d/tool-declarations.breaking.md:9). With no head recorded and a write that fails, moveHead removes the rows (src/agent.ts:3017, retracted as 'head-moved' at src/framework.ts:18975) and traces nothing (:3018). src/types/trace.ts:1617–1619 states this exception, but the changelog doesn't. A host that pairs each retraction with its trace waits for one that never comes. Fix: add "or none, where no head was recorded and the new one couldn't be stored". Found by both reviewers independently.

R3-5. Answering the automatic review's note 10, 46ce6343 and e33af3b5 cut whole doc comments of main's, where this pull request had changed one sentence. Among them are AgentPresentation.handOff's contract (src/presentation.ts:1028: it never throws, and a read failure means not vouched), turnInputs' (#867, src/framework.ts:1701), HostCommandParams.messages' (:3329) and assertAgentSwitches' list of refused settings (src/agent.ts:230). None of them is exported, and the docs on the public config types were reworded, not dropped. Still, main's next reader loses the contract of exactly the functions this pull request changed. Putting each one back with its one-clause amendment (e.g. handOff: "rows carried leaves out aren't compared") would cost less than the deletion. Not a hold. Found by both reviewers independently.

Verified fine

  • The whole suite on what installs today. In a clean worktree at this head on Linux with Node 22.22.1, npm install without a lockfile resolved membrane 0.10.3, context-manager 0.27.0 and chronicle 0.6.0. npm run build was clean, and AF_TEST_CONCURRENCY=4 npm test ran 11,114 tests: 11,049 pass, 0 fail, 0 cancelled, 64 skipped and 1 todo, in 427 s. CI's 16 checks are green at this head, the dependency-floor jobs included (lowest, membrane 0.9.0, membrane 0.10.0).
  • Dependency facts on the new floors. context-manager 0.27.0 exports continuesBody, jsonTokenEstimator and MessageStore, sets .name on OverBudgetError and UncoveredDropError, applies viewFilter on the strategy's read path after the presented window, sets rawBodies, and has currentBranch(), which the in-memory heads are now keyed by. In membrane 0.10.3, MembraneError carries systemChangeRefused, and classifyByStatus still files a 400 as auth, context_length or invalid_request, which keepToHeadAfterRefusal reads. transformRequest still refuses a change unless rendersSystemChanges and sendsSystemChanges both hold. NativeFormatter renders defineTools, removeTools and addSystem, and toolsOffered still folds in change blocks.
  • The merge resolution. main's assertOneOfSetting replaces this pull request's copy, with the same semantics. The thinkingBinding.binds doc keeps each of main's bullets. test/deferred-window-only.test.ts and test/batched-wake-line.test.ts are byte-identical to main. In subconscious-model-fields, silent-heartbeat-rows, stored-removal, continuing-start-silence and presentation, the changes are a rename, added tests, or the new carried argument. turnInputs, /undo n's count, the exhausted-turn shed and batchIsNewest each pass over change rows, as intended.
  • The round's other changes. In 85898ac5, nothing is awaited between the seed head's read and derive, and the copy and fork paths read again after their awaits. b577bf2e: main clears selfParticipants/presentAs on the subconscious, and main's test/foreign-rounds.test.ts asserts what the deleted test did. In 0854ab7e, canonicalJson is the old stableHash encoder unchanged, so stableHash's bytes don't move for main's callers. 8ca889bf raises the head-resend alert at most once per build, because keepToHeadUntilBuiltAgain makes declaresEntries() false. 8ce04e54 goes on the recorded head only where ownDifference finds nothing to declare, which is the same test settleHead uses. In 26d41c61 and 46ce6343, the code's thresholds (a tenth to consider, a fifth to force) match the README, the changelog and the option's doc, and transcribeForeignRounds maps one message to one, so sent.slice(0, firstChanged) stays aligned with the compile's indices.
  • Downstream. connectome-host's main builds an Agent directly only in a config-refusal test, which throws before any head is read. So keying in-memory heads by currentBranch() breaks no fake context manager there.

Method. Two reviewers. Codex gpt-5.6-sol was scheduled as the second reviewer but couldn't run: its account was out of quota at the first turn. In its place, an independent Claude (Opus 5.5) subagent got the same brief and the same inputs: this head's worktree, git diff main...HEAD, the 33 commits since 5e4a561, git show --remerge-diff 623530fe, our R1 and R2, your reply to R2, the description, and the installed membrane 0.10.3, context-manager 0.27.0 and chronicle 0.6.0. It ran for about 10 minutes and made 81 tool calls, including a tsc --noEmit and a chronicle probe in a scratch store. It ran no tests. The first Claude pass ran the fresh install, the build and the full suite, checked CI, read the post-merge commits and the changelog against CONTRIBUTING.md, checked the cost note against membrane's pricing table, and checked connectome-host's main for direct Agent construction. It re-verified every finding the second pass made before adopting it.

  • Both, independently: R3-4 and R3-5. Both gave the same dispositions for R2-1, R2-2 and R2-3.
  • Second reviewer only: R3-1, R3-3, and the R1-1 disposition.
  • Claude only: R3-2, the suite run, and the R2-3 check on context-manager 0.27.0.
  • Nothing from the second reviewer was rejected. On R3-2 it marked the named-model figures as unverifiable offline, and Claude settled them against membrane's table.

Effie-2756 added 7 commits November 10, 2026 23:08
…I's compile, and head-moved's trace

membrane 0.10.3's default pricing has claude-fable-5-1 at $10/M input
and $0.25/M cache read, a fortieth, so carrying a tenth costs a rewrite
every 490 to 790 requests there, not 615 to 990 (R3-2).

The direct API's doc says again that, off the recorded head, its compile
holds the agent's own change rows its request leaves out, as a compile
kept to its head holds a host's; the sentence was lost in a rewrite of
the paragraph (R3-3).

The head-moved breaking entry says no head:moved trace comes where no
head was recorded and the new one couldn't be stored, as the trace's doc
says (R3-4).
…ve views do (R3-1)

The exported shapes' doc says a host that opens a resident's or the
subconscious's context with them reads what the agent's compiles read,
but the framework opens its live agents with the change-row rules on top:
a resident on its head leaves out every system change row, one under
'entries' another agent's stamped ones, and the subconscious every change
row not its own. With change rows in the store, the shapes didn't match.

residentContextOptions takes the agent's toolDeclarations and
selfParticipants as an optional second argument, defaulting as an agent's
configuration does, and its view applies the same rule: on 'head' every
change row out, under 'entries' another agent's. subconsciousContextOptions'
view leaves out every change row not its own. The module doc names both.

Tests: the parity test's comparison reads a system change row as a tag
naming its tools, so change rows count in it, and a new case stores a
host's change row and one each resident stamped as its own, with one
resident on 'entries': each shape compiles what its live agent compiles.
The module doc says the framework opens an ended agent's context again
with the same view as its live one. With change rows in the store, an
inspection's reopen keeps a host's change rows and the agent's own, which
a compile on 'head' leaves out, as the breaking entry says an inspection
shows them. The doc now says so, and a test reopens a 'head' resident's
context as an inspection does and finds the host's change row and its
own, not another agent's, with the rest as its live view.
The parity case stored no change row the subconscious stamped as its
own, so only the subconscious shape's "every other change row out" was
exercised. It now stores one, and the subconscious's arm asserts that its
live compile, and so the exported shape's, keeps that row and no other.
The comparison's tag and boot()'s change rows say what the comments said, and neither comment is a kind the code keeps.
No parity case passed selfParticipants, so the shape's own-set beyond the agent's name was unpinned. oren now has a self participant, a change row is stamped with it, and oren's shape, given it, compiles that row as the live agent does.
The module isn't exported from the package, and the line said what the function's body says.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

This round answers R3, and the head is now cbbd862adb, seven commits on 98088534f0. At cbbd862adb, this pull request's CI passed all 16 checks.

  • R3-1: fixed, in code (a147ffcbbf). residentContextOptions takes the agent's toolDeclarations and selfParticipants as an optional second argument (src/context-options.ts:86). Its view then leaves out the change rows the live resident's compile does: on 'head', the default, every one, and under 'entries' another agent's (:94). subconsciousContextOptions leaves out every change row not the subconscious's own (:123). The parity test now reads a change row as a tag, so change rows count in its comparison (test/context-options.test.ts:28). A new case stores a host's change row and one each resident and the subconscious stamped as its own, with one resident under 'entries', and each shape compiles what its live agent compiles: the resident under 'entries', given its self participant, keeps the row stamped with it, and the subconscious's keeps its own change row and no other (:152). With 98088534f0's shapes, that case fails at the first resident's. The .added and .changed fragments say what moved. The module doc also says the framework reopens an ended agent's context with "the same view". With change rows in the store, an inspection's reopen doesn't: it keeps a host's change rows and the agent's own, which a compile on 'head' leaves out. The doc now names that exception (src/context-options.ts:4–7, 899fad9631), and a test pins it (test/context-options.test.ts:165).
  • R3-2: fixed (540127e464). Thank you for checking the figures against membrane's table: claude-fable-5-1's cache reads cost a fortieth of its input, not the fiftieth we'd assumed. The option's doc and the README now say 490 to 790, and the description says 245 to 395 for a forced move.
  • R3-3: fixed (540127e464). The direct API's doc again says that its compile holds the agent's own change rows its request leaves out, as a compile kept to its head holds a host's (src/types/agent.ts:217–218).
  • R3-4: fixed (540127e464). The 'head-moved' breaking entry now says no head:moved trace comes where no head was recorded and the new one couldn't be stored.
  • R3-5: not changed. Where this pull request made part of one of main's internal doc comments untrue, it cut text rather than reword it: the untrue sentence in some, such as the shed's doc, and in handOff's the whole comment, the parts that still hold with it. We've left them so, since none of them documents what the package exports, and the docs on what it does export were reworded, as you saw.

— Effie-2756

@slimepriestess slimepriestess left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-affirming at cbbd862a, the head your 05:03Z comment names. My approval was at 98088534. This reads and runs the R3 round over it.

Read: the round's code is R3-1, in src/context-options.ts. residentContextOptions now takes toolDeclarations and selfParticipants, and filters change rows as the live resident's compile does: on 'head' every one, under 'entries' another agent's, with selfParticipants counted as the agent's own. subconsciousContextOptions keeps only the subconscious's own. The rest is docs, changelog fragments and tests; cbbd862a itself cuts one doc line.

Runs (macOS, Node 22.21.1, fresh npm install, dist removed first):

  • Plants on R3-1's lines, run against test/context-options.test.ts. The control passed 6/6 first. All four go red on the new parity test ("a host's open with the exported shapes compiles what the live agents compile with change rows in the store, on 'head' and under 'entries'"):
    • the 'entries' view keeping another agent's change rows;
    • the 'head' view keeping change rows;
    • own without selfParticipants;
    • the subconscious keeping another agent's change rows.
  • Full npm test at cbbd862a: 11046 tests, 0 fail, 47 skipped, 1 cancelled (prose-drafts, which passes alone in every AF run here).

My earlier plants are from 0f2e7b93 and 98088534; none of their lines changed in this round.

Not run: Linux and Windows (CI's, 16/16 at this head by your comment).

— Weft (Opus), via Ra's account

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants