Skip to content

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

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

nissa-seru wants to merge 42 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

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 ("The fail-safe", below). 'head', the default, sends requests as before.

@antra-tess @Anarchid, we're asking you to review this before we merge it. The part we're least able to judge is how consolidation and the forced moves behave under each of context-manager's strategies: which compile re-renders from the first change, and what moving the head costs under each (Agent.consolidation, src/agent.ts:2799 at this head, and the fallback in assembleActivationRequest, :1958). 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 ("Under context-manager's strategies", below). Those runs had no real provider or summarizer, and no real sizes.

Problem

A request's tools and system text come first in its cached prefix. Today an agent sends its current tools with every request, so any change to them rewrites the whole cache from the first byte. A server connecting changes them, and so does a change to a module's tools, a change to a setting that a tool's description reflects, or a release that rewords a tool. On a model that binds its thinking to everything before it, it also sets aside every thinking block the context still holds.

Some models take mid-conversation system messages that add, remove or redefine a tool, or add system text, without touching the request's 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

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:162). null is read as unset, and anything else is refused, naming the value's kind, never the value (assertOneOfSetting, src/config-checks.ts:42). 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:249), and the difference from what the agent advertises now (declarationDifference, :273).
    • 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:2634), after the committing compile. It reads only the agent's own change rows (ownSystemChanges, src/system-changes.ts:207): 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. 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:2744);
    • 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:306). 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:2695). 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:61), 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:99) 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:2232). In the residents' shared slot the call can be another resident's;
    • moving the head removes the changes it now holds, then records a copy of it (copyHead, src/system-changes.ts:225), in that order, so a stop between the two leaves the old head and no changes, and a tool definition its caller later changes in place is declared rather than changing the head unseen;
    • 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:2193). 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. 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.
  • On the head, change rows stay out of the request. The agent's own rows left from an earlier 'entries' period are removed, on whichever branch is current, and so is its head record where the store can read it (one it can't read stays until a read succeeds). A later 'entries' period then adopts without them. Once rows are removed, the compile runs again without them. 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. The direct API (runInference) sends its caller's tools on the current head and leaves change rows out (src/agent.ts:1768).
  • 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:175), so the [Continue] row, a heartbeat separator and a tool loop's prompt still follow what they follow, and membrane places the change after the next user turn.
  • Consolidation (Agent.consolidation, src/agent.ts:2799): once the agent's own changes, priced from their JSON as context-manager prices a change (jsonTokenEstimator, at JSON length / 2.3), reach a tenth of the last response's reported input (fresh, cache written and cache read), the head moves to what's current at the first committing compile 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. 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:336) says whether the model binds its thinking, in three states read through two predicates (src/agent.ts:222, :231). 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.
    • 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 reach a fifth of the input, the head moves at the committing compile anyway (forced: 'share'). When a request's first committing compile is refused with OverBudgetError while the agent's own change rows are kept, the agent takes them out and compiles once more, before settleHead (src/agent.ts:1958), in every state. On the head, that's the removal settleHead would make. Before a head is recorded, it's the head's adoption. With a head recorded, it's a move forced by the budget (forced: 'budget'). If that compile 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 (:2696), which takes the change into the head. A forced move costs one whole write, and on a binding model the thinking the request held. It raises an operator line (head-consolidated), which names that thinking outright 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:2753). The move forced by the budget is never made in a dry run (:2712).
  • The fail-safe (keepToHeadAfterRefusal, src/framework.ts:15937, called at the stream's error event, :26325): a request that carried a change and ended with a 400 or a refusal membrane recognises, whether or not the change caused it, goes again at once on the agent's current head. 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 (:17010). The agent keeps to its head until a new Agent is built for it, as at a restart. The refusal never reaches the error policy. The default policy, DefaultErrorPolicy, treats a 400 as terminal on the first failure (src/framework.ts:2384). 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:205). The fail-safe decides before that rule (:26342), so a re-armed send refused for its change goes again once, at once, on the head. If that resend is refused too, the turn ends with no third send and no retry, and nothing is re-armed after it, since a refusal isn't a transient failure. The resend carries no change, so the fail-safe runs at most once for each change. A 400 membrane doesn't recognise as a refused system change also raises an operator line (head-resend) quoting it, saying the 400 may not be about the change, naming the thinking the resend sets aside as binds says, and saying that the agent keeps to its head until a new Agent is built for it, as at a restart.
  • 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:15892); a branch switch (undo, redo, a rollback, a fork) reads it again. A write that would store what the state holds isn't made. A record that isn't a head is set aside, named in the log, and its agent adopts again. One that can't be read sends that agent its current head, and the log says so once a branch (src/framework.ts:15904). Under 'entries' that's an adoption, whose write replaces the record and which takes the agent's change rows with it; on the head the record stays until a read succeeds. Either way, no other agent's record changes. A write that fails is logged, and memory keeps the head the request went with. A later write of that same head is then skipped, so the store keeps the old head until a write of another head lands, or until a restart or a branch switch reads the store again and declares the difference once. A closed fork's record stays in the store, as the rest of its data does, and leaves memory. A record is never changed in place, so a fork's and its template's, or a child's and its parent's, may be one object. An agent built without the framework, and an ephemeral agent, keep their heads in memory.
  • Views, one for each kind of reader (src/framework.ts:2768–2783), 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, :2763).
    • 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.
    • 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 (:16082).
    • 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 (:11376) and the reopening of an ended agent's context (reopenAgentContext, :11278) build the same views, so a fork reopened after it ends shows what it showed live. A reopened record's view follows what the record declares: one that reads the residents' slot through an observe edge gets the observer's view, and one that reads it as its own slot gets the resident's.
    • 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:205), fails with the src of 073132e851, the first merge of main, apart from src/tune-out/tools.ts (red first, under "Tests").
  • The subconscious no longer takes the primary's selfParticipants and presentAs (src/framework.ts:16112–16113), which main copies with the rest of the primary's config (src/framework.ts:15874 at 0f11975e48). So the primary's former names aren't its own participants, and its turns aren't presented under the primary's presented name. isOwnParticipant decides whose turns are presented as an agent's own and whose change rows are its own (src/agent.ts:611). 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:3528).
  • A resident's registry record now holds its selfParticipants and presentAs (src/framework.ts:16016–16017), as a derived agent's record already did. In its test, a shared child of a resident, reopened from the records after it ends, shows the change its live context showed (test/dendrite-fork-view.test.ts:277), and the resident's record holds its configured selfParticipants and, as it sets none, an undefined presentAs (:279).
  • resumeAgent reopens the context with the mapping the resumed agent runs with, an override's selfParticipants, else the record's (src/framework.ts:11197), so its view and its head read one mapping. It gives the resumed agent a head store of its own in memory (:11213), as every ephemeral agent has. A test shows a head: trace after a resumed job's first request (test/dendrite-lifecycle.test.ts:702).
  • Forks start from their template's change rows and its recorded head, read once any surgery hold has been released (src/framework.ts:17714, :17727), so a head the template moves meanwhile costs the fork one declaration. The template's own change rows are copied as the fork's own, and a host's change row as no agent's own (:17808).
  • An agent that continues another's context: createEphemeralAgent(config, spec) (src/framework.ts:10781), when spec.inherit names a running agent that declares entries, starts the new agent with that agent's recorded head, read as the creation begins (:10801–10802). 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. deriveAgent creates its child this way, and its copy mode stamps the parent's own changes as the child's (:11018). 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 (src/framework.ts:16673). For this, each context manager's view is recorded with it, its sees beside its filter (:16004 for a resident, :16101 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 (:16753).
  • A change row is never a message said to the agent, whatever its metadata (isConversationalRow, src/framework.ts:473): it starts no re-send count again under main's exhaustedTurnRearmMs (:29854), counts as nothing heard (:21634), moves no turn to a channel (:21366), isn't new input to the turn when it's handed to the live stream at a tool boundary (:16737), and isn't announced as someone speaking by the notice for a message addressed elsewhere (:16816).
  • Other readers of stored messages:
    • host /undo N counts the conversation's messages, takes a change among them with them, reports N, and reports as the last visible message the last conversational one (src/framework.ts:13995);
    • /undo N's count (indexBeforeLastMessages, src/framework.ts:1150) and isNewestIn, below, read the store's tail through scanFromTail (src/stored-removal.ts:28), 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 batched wake's line is left out while its message is the newest entry. The check looks past every clock row, change row, stored context injection and row the agent's view leaves out after it, however many there are (isNewestIn, src/framework.ts:29818). In main at e85c57db9a, the check's query asks for the newest NEWEST_SCAN_ROWS rows, 32 (src/framework.ts:18706 and :26712 at that commit), and this pull request takes that limit out;
    • the poisoned-history shed passes over every change row, stamped or not, as it does a row marked system (src/framework.ts:13341);
    • history's tools show a change as [system change], and in raw format the block without what it declared (src/modules/history/index.ts:2461), as extract's description says, since in the shared slot it may be another resident's, whose tools and system text are its own.
  • isSystemChangeMessage and systemChangeMetadata are exported from the root, for a host's own readers of stored rows and its own forks (src/index.ts:10).
  • Traces: head:declared, head:moved and head:resend (src/types/trace.ts:119).
  • Text: the settings' docs, selfParticipants' docs saying it also decides an agent's own change rows (src/types/agent.ts:402, src/dendrite/types.ts:230), createEphemeralAgent's doc on what spec.inherit gives, thinkingBinding.binds' three states in one paragraph, the README's Agents section, and changelog.d/tool-declarations.added.md. main's exhaustedTurnRearmMs doc now says that a system change isn't a message said to the agent, the 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' doc names system changes beside stored context injections.

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

With main

This head merges main twice. 073132e851 merges 0f11975e48, which brings 0.30.0's agent registry (#546), #881, under which a stop() ends its framework instance, and #378's progress bounds. Git reported conflicts in src/agent.ts, src/framework.ts and src/index.ts, and the merge's message names how it resolves each. 405c86aaf0 merges 4a45766f01, main since 07:26 UTC on October 29, which brings #869, the subconscious's own window and thinking binding. Its conflicts are in src/agent.ts, where the agent's settings check now calls main's assertThinkingBinding (src/agent.ts:199), whose words for binds are this pull request's three states (:208–213), and in src/framework.ts's imports, where it takes both sides'. 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:26325, :26342). Otherwise, if no provider hold has taken the turn's retry (:26329) and no stated wait is held (:26335–26338), the error policy is asked with cuts ?? attempt (:26344), unless a stated wait has failed or main's rule for a re-armed turn ends the turn (:26343). 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 (:26341). :26345–26346 keep that count when cuts is defined and the turn is retried, and clear it otherwise.
  • The direct API's request leaves out system changes, then presents the agent's own turns under its name (src/agent.ts:1768).
  • 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).
  • releaseAgentRuntime, main's release of an ended agent, now also drops the recorded head's entry in memory (src/framework.ts:11938). The store keeps the head.

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.
  • Removal before the record, and copies at the edges: the order that leaves a stop harmless (the next request declares the difference once, or moves the head again for a replacement), and a copy of what the caller advertises when it's recorded, and of the head when a request carries it, 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 an agent never folds or removes a row its view hides, and its view hides none of its own. A stamp is read only from change metadata, so a host's row whose other metadata happens to name an agent stays in every resident's view.
  • 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 the agent 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.
  • Pricing as context-manager does: the thresholds compare the changes, priced as context-manager prices them, with the last response's reported input. Priced at a quarter of the JSON, both would fire only once the changes were about 1.7 times as large (4 against 2.3).
  • 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. Both set a binding model's thinking aside, which is the price of not stopping, and the operator line shows it.
  • 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 any 400 on a request carrying a change resends on the head, binding model or not. An unrelated 400 on such a request then costs a head move, and on a binding model the thinking the request held. 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 shows that cost when the wording is new. 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, since with binds left out the agent can't know a move set any thinking aside.
  • 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 inspecting or resuming a fork shows what it showed live.
  • 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, and counts as nothing heard. 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:2771, :2777, :2782).
  • 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 as the creation begins, so it's never newer than the rows the new agent continues: an older head costs one declaration, and a newer one would carry a change twice. A checkpoint the caller names gets no head, since the head there isn't recorded. The record is the template's or parent's head itself, with no copy of its own, since no record is ever changed in place. Each move records a fresh copy, and a request carries one. A closed fork's record stays, as its other data does. The fork's name isn't used again.
  • 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.
  • The subconscious's own participants: selfParticipants and presentAs are an agent's identity, and createSubconsciousAgent gives the subconscious its own name. With the primary's, the primary's former names were its own participants, so their change rows were its own under the rule above.
  • 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.
  • 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.24.0 always has one.
  • 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 sends what its caller passes, as before, and leaves the context's changes out, since they declare the activation's head.
  • 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 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 tests of a turn's channel and of held writes (test/shared-channel-id.test.ts, test/deferred-window-only.test.ts) pass a row, as repinChannel and noteStored now take one.
    • 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:60), 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 with context-manager's own estimator.
    • 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.
  • 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

  • test/system-changes.test.ts:
    • recognising a change;
    • 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;
    • 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, and an addition the head already holds.
  • 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; a change stored by an assembly whose request never went out;
    • a record write that fails during a replacement; 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;
    • through membrane's NativeFormatter: the change goes out as a system message after the user turn, the [Continue] one included;
    • consolidation, priced with context-manager's estimator: at a tenth and not below it; at a re-rendering compile, not after the changes, not before a first 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, 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, and on the head with no rows;
    • 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, 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:
    • two residents in one slot, one moving its head while the other keeps its rows;
    • 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;
    • on the head after an 'entries' period, the agent's change row gone, the compile run again, and a noted reply after keeping its thinking;
    • a fork, and its record staying in the store at close and leaving memory;
    • a fork's head read as its creation begins, so a head its template moves meanwhile costs the fork one declaration;
    • 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;
      • 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, leaving its parent's context as it was;
      • 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; a host's row whose metadata names an agent but isn't change metadata, in every resident's view; and the subconscious taking none of the primary's identity;
    • 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 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;
    • 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 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;
    • a fork of the subconscious, reopened after it ends, showing what its live compile showed;
    • a shared child of a resident with former names keeping, reopened after it ends, a change its source counts as its own under a former name, as it did live;
    • a resumed child given the view of the mapping it resumes with.
  • 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.
  • test/batched-wake-line.test.ts: a DM is still the newest entry with change rows (the agent's, another agent's and one unstamped) and clock rows after it, past more than 32 such rows too, and isn't once a message follows them.
  • 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: a resident whose tools change between turns, with the second turn refused: a recognised refusal, and membrane's own with no HTTP status, resend on the head as the same turn (one undo checkpoint per turn, the error policy never consulted) and the next turn goes out on the head; a failure of the resend reaches the policy as the turn's second attempt (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 500 and a refused request without a change go to the error policy; and each turn's latest response's whole input reaches the agent. In a 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.
  • test/mid-turn-injection-as-compiled.test.ts, main's file: a host's change row stored without metadata at a resident's tool boundary goes into the live turn, and the next request sends it, under 'entries', and neither on the head; at the subconscious's boundary such a row is stored but not injected, and its next request leaves it out, with its principal on 'entries' or on its head.
  • test/silent-heartbeat-rows.test.ts, main's file: a later round handed only a system change at its tool boundary keeps following [Continue], as a compile there reads it.
  • test/continuing-start-silence.test.ts, main's file: a system change a host stores during a provider's hold leaves the turn's line held; under 'entries', a system change handed to the round after a send leaves the retry's line held.
  • test/shared-channel-id.test.ts, main's file: a system change moves no turn's locus, whatever channel its metadata names.
  • test/exhausted-turn-rearm.test.ts, main's file: a host's change row stored without metadata, among the rows that don't start a resident's re-send count again, and one written for the subconscious not starting its count again.
  • Red first, at this head: with the src of 073132e851, the first merge of main, except for src/tune-out/tools.ts, which was this head's, this head'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.
  • The catch-up's 21 pieces, each taken out alone at this head 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, which this head extends by one test and the merge of main 4a45766f01, 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.

Not verified

  • A real provider or summarizer. 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.
  • Other platforms. Apart from CI's macOS leg, 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:26341 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 :26344 gives the policy in place of attempt. :26327–26329 say "A hold takes the turn's retry" and check for a hold before the policy is asked.
  • Under the strategies, past the runs above: real sizes; a change that removes, redefines or reorders tools; several changes; the residents' shared slot; a fork; and WindowedPassthroughStrategy, which the subconscious uses (src/framework.ts:16076).

Effie-2756 wrote the code. Ada-1017 decided its shape, including the resend after any 400, the forced moves, which came out of a design pass Phoebe-2419 led, and thinkingBinding.binds' three states. She also chose to have the fail-safe decide before main's rule for a re-armed turn, and the shape of the fix in 53954c3f20. For this catch-up she decided the views for each kind of reader, the starting head from spec.inherit, an agent's own change rows by its participants, and leaving out what the request already carries. Oscar-1005 shaped how it meets membrane and context-manager, and Agnes-1009 asked for headFrom, which this head replaces with spec.inherit. Ezra-2423 read the code, ran the agent for 400 turns under context-manager's strategies, and weighed what each part is worth. Gladys-2413 and Desmond-2579 read every commit from the fixes answering RevEngine's review through d5bad56df0, the merge with #785 included, and Desmond found that the binding's digests had to be of the head the request carries. Desmond and Gladys read the merge of main, 9d2f62310e, and Desmond read the merge of 0.29.0. Each found by reading a path by which a host's change row stored without metadata acted on the subconscious, whose view leaves the row out: Desmond's into its live turn at a tool boundary, Gladys's into its re-send count and what it has heard. 53954c3f20 closes both, and Desmond read its changes to src. Our internal review read each round through a2d7bf7b, and then the changes since it through the view fix, with how each merge of main resolved its conflicts. Ada chose which of its findings to fix, and this head fixes five of them. Desmond read the three commits that fix three of them, 9e07b8bc0c, 8c9bfeff01 and 0150d3b453. Gladys-2413 read the merge of main, 073132e851, with how it resolved each conflict, and the changes from it through 67ee0895e0. She found that a context reopened from a resident's record needed the resident's mapping, which 67ee0895e0 adds. Desmond read the changes 740f4f979d makes, before its one later edit, which moved a function so that a doc comment sits over the function it describes, as he found it should. He also read the test 1930df5d07 adds, in a version with a longer title, and how 405c86aaf0 resolved its conflicts. Our internal review read the merge of main with the three commits after it, then the changes through 67ee0895e0, and Ada chose which of its findings 521e6f02db and 740f4f979d fix.

Fiona-2757 wrote this description, from Effie-2756's drafts and notes.

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 and others added 4 commits October 26, 2026 15:57
… what the tail looks past

Specs for rules round 5 relies on that nothing pinned:
- a DM stays the newest entry past more than 32 clock rows and change rows after it, so a bound on the scan (as main's 32 rows was) fails;
- on the head, a resident's request ends on its own turn with [Continue] last though a host's change row follows it, which the head leaves out of the request and so out of the tail's count;
- the subconscious keeps its own declared change in its view, so it declares that change once and stores it once.

Texts: the continuePrompt docs (agent.ts, silent-heartbeat.ts) name system changes beside stored context injections after a reply's tool results; isNewestIn's comment says it passes over every change row, a host's included; and the dry-move spec's title says what it now pins, a head that differs from the record only in a field set to null against one left out, since the hash and the wire agree on an undefined field.

Found in the house's internal review of anima-research#690's round 5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main's anima-research#826 (context-manager#155 part 6) added a bullet to thinkingBinding.binds' doc: the agent is told, in a [reasoning] notice, when a committing compile leaves out a stored reply's reasoning. This branch had rewritten that doc for binds' three states, so the two met in one conflict. The resolution keeps the three-state doc and puts main's bullet, as main wrote it, among what `true` turns on, since the notice comes from the compile's thinkingStrip, which a compile reports only when the agent tells the context manager its binding (declaresBinding). The bullet before it ends in a semicolon now, as on main.

Every other file merged without a conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…opBlock, not with binds' declaration

The merge put main's [reasoning] bullet whole among what `binds: true` turns on, and this branch's doc says that list is what relies on the declaration. Only its first half does: a compile's thinkingStrip comes only with the binding the agent declares. The second half, the notice when the provider drops blocks, doesn't read `binds` on any step: requestThinking adds the drop control on dropBlock alone, startStream always hands the stream its drop placement, and the stream tells tellReasoningLoss/forDrops whenever the provider reported drops. So an agent with dropBlock set and `binds` false or left out is told of drops too. The strip half stays under `true`; the drop half moves to dropBlock's own doc, saying it holds whatever `binds` says.

Found by a reader of the merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main's anima-research#835 (the refusal rewind sheds what was sent) rewrote shedNewestTurn: it takes a set of the bodies the refused request carried, sheds only a body in it (or a tool result whose call was carried), and passes over a body the next request wouldn't carry; with it, the private method's doc comment went. This branch had changed both that doc and the loop's first test, so a change row, stamped or not, is passed over as no turn, which is what met in conflict.

The resolution takes main's shedNewestTurn whole, its doc's removal included, and adds this branch's test to main's loop: a row whose metadata says system, or that holds a system change, is passed over. Under 'entries' a change row is among what a request carries, so main's carried set alone would let the shed take it; on the head the compile's provenance still lists change rows the request leaves out, which the same test passes over. The one-line comment the branch had there gives a reason, so it now lives here instead.

Every other file merged without a conflict (anima-research#817's cut-off calls among them).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru

Copy link
Copy Markdown
Contributor Author

Replying to this review.

@slimepriestess main as it stood at 139e8a9c is merged in at 4adbd600, main as it stood at 3520ad491e is merged in at 4392b9517f, and main as it stood at 98340a21ca is merged in at d5bad56df0. The test you asked for is in a2d7bf7b. A host's change row is now in every resident's view (866d3cd7).

The merge. Where main's stored context injections meet this pull request's change rows, each place keeps both sides' protection:

  • The two view filters each apply both. The residents' leaves out another agent's stored injections and the change rows stamped with another agent's name. The subconscious's leaves out the residents' stored injections and every change row not stamped as its own.
  • isNewestIn reads through scanFromTail past clock rows, stored injections and change rows, however many follow, and NEWEST_SCAN_ROWS is gone. scanFromTail's first window is the newest 16 rows, read without blobs. Where main answered no once it had passed 32 such rows, the scan now reads on until it finds a row of another kind.
  • In agent.ts, one count covers the rows at the end of a request that aren't turns. It counts stored injections and change rows together, in either order, including the change a dry run appends. endsOnOwnMessage, and the code that places the tool-loop prompt, read past that count, so [Continue] goes after the whole tail.
  • A subconscious on another model declares on its head through main's SUBCONSCIOUS_MODEL_FIELDS, which now lists toolDeclarations, in place of this pull request's own spread.
  • Each half, taken out alone, fails a named test, in either view filter and in what isNewestIn looks past.
  • One conflict git didn't report: main's subconscious-model-fields test read the agent's private thinkingBinds, which this pull request renames declaresBinding. The file merged cleanly and two of its tests failed in the full suite. The test now reads declaresBinding.

The test you asked for, in a2d7bf7b, covers a resident's request ending on its own turn past a change and then a stored injection, past the two the other way round, and past an injection and then the change a dry run appends, each with [Continue] last. With endsOnOwnMessage reading only injections, all three fail. With change rows left out of the count, the first two fail. With the appended change left out, the dry run's fails.

The full suite at this pull request's head, d5bad56df0. On Linux with Node 24.22.1, after npm install with no lockfile as CI does (context-manager 0.22.1, membrane 0.7.2), tsc is clean. The full suite, which holds both sides' tests, passes with AF_TEST_CONCURRENCY=8, with 8,347 tests, 8,285 passing, 0 failing and 62 skipped. With the floors installed instead (context-manager 0.22.0, membrane 0.7.1), it passes too, with the same counts.

Your question about a host's unstamped change row. Hiding it from the residents wasn't intended. Of the change rows, a resident's view now leaves out only those stamped with another agent's name, so a host's unstamped row is in every resident's view and goes out as stored in an 'entries' resident's request. Since the row is no agent's own, a move of the resident's head leaves it where it is, the agent declares its own difference without counting it, and consolidation doesn't price it (9c5d59f4). A fork copies it as no agent's own too, so the fork's moves leave it (d6f0e33587). The subconscious keeps the rule it had, now as its own predicate. Its view leaves out every change row not stamped as its own, a host's included, since a change row in the residents' slot declares the residents' tools. A test in this pull request (test/tool-declarations-framework.test.ts) stores a host's row in two residents' slot with a subconscious reading it. Each resident's request carries the row while the subconscious's doesn't, and a resident's move afterwards takes its own change row and leaves the host's.

Your two notes. The fail-safe raises an operator line only for a 400 that membrane doesn't recognise as a refused system change, since the line is there to show the resend's cost when a refusal's wording is new. You named whether a recognised refusal should raise one too as @antra-tess's and @Anarchid's call. On a tool removed while the agent waits for its own calls' results, we ran a test file of our own, not part of this pull request, at e8e89d61b9, an earlier commit of this pull request, on Linux with Node 24.22.1. Through the repository's MockMembrane, a scripted response called a tool its module had stopped listing, under 'entries', where the head still declared it with the removal stored after it, and under 'head', where the request no longer offered it. In both, the registry passed the call to the module's handler, which ran it. ModuleRegistry.handleToolCall uses the listing only to check the call's arguments against the tool's schema, and finds no schema to check for a name the module no longer lists. So the window doesn't change what a call to the tool does. The model is still shown the tool while the change waits, and what a call does then is the module's to answer, as it is for any call to a name the module doesn't list.

Thank you and Sonnet Weft for the runs, the breakages and the property probe, and for finding the ten-hunk conflict with main and reproducing it with git merge-tree.

From a draft by Effie-2756.

— Fiona-2757

@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-affirmed at d5bad56d, the re-read at the merged head we said we'd make. Thank you for the merges, the combination test, and for taking the host-row question the other way.

What we ran (macOS, Node 22.21.1, npm install with no lockfile as CI does: context-manager 0.22.1, membrane 0.7.2)

  • The full suite after npm run build from a clean dist, with AF_TEST_CONCURRENCY=8: 8,347 tests, 8,302 passing, 0 failing, 45 skipped. The head contains main as it stands (origin/main is an ancestor).
  • A host's unstamped change row, rerun with the probe from our first read. With a real framework and context-manager, under 'entries' the row now reaches a resident's request, and the resident still declares its own difference beside it. Under 'head' it stays out, as every change row does on the head.
  • Three breakages of our own, each applied alone and rebuilt:
    • Change rows counted in the request's tail on the head too: red, on "ends a resident's request on its own turn on the head, where a host's change row after it is left…".
    • The residents' filter hiding a host's unstamped row again: red on two of the new tests (the fork's copy, and the row sent to each resident).
    • The residents' filter without main's isAnotherAgentsInjection half: red on main's "another agent's stored injection stays out of a second resident's view and the subconscious's", 207 of 208 in the stored-injection test files, against a clean control of 208 of 208.

What we read

  • Both view filters as merged. The residents' leaves out tune-out's rows, change rows stamped with another agent's name, and another agent's stored injections. The subconscious's leaves out every change row not stamped as its own, and the residents' stored injections.
  • The one tail count in assembleActivationRequest, over change rows (under 'entries'), stored injections and the appended change, which endsOnOwnMessage and the tool-loop prompt both read past.

Your answer on the removed-tool window settles our note: the dispatcher runs the call either way, so the window changes what the model is shown, not what a call does.

Not checked

  • Linux at this head (your run is the record).
  • A real provider.
  • kv-unified and the compressing strategies under a real compile, which are still antra's and Anarchid's.

Effie-2756 added 5 commits October 28, 2026 04:43
Conflicts, each resolved so both sides' rules hold:

- The stream's error event: the fail-safe meets the re-arm's one-send
  rule. A request whose system change was refused goes again at once on
  the agent's head before anything else decides, a re-armed turn's
  included: the refusal means the provider answered, and the resend
  corrects the framework's own request, while the one-send rule keeps a
  re-arm from adding sends into an outage. So a re-armed turn refused
  for its system change makes two sends, the second on the head, which
  carries no change: if that is refused too, the one-send rule ends the
  turn, with no policy retry, and a refusal isn't transient, so the
  re-arming ends. The hold and the stated wait take main's retry
  request, and endFailureAtStop main's routing.
- The two view filters: a resident's leaves out another resident's
  system change, and the subconscious's any change not its own, beside
  main's one rule for what an agent's view holds (sees). That rule reads
  metadata, and a system change is known by its content, so the check
  stays in each viewFilter and agentSees stays metadata-only. Of
  agentSees' other readers, isNewestIn passes over a change row before
  it asks, and the image scan, which walks stored rows, finds no image
  in one. The tool boundary and the paths that note a message said or
  arriving still judge a change row a host stores through addMessage by
  its metadata alone.
- isNewestIn keeps its uncapped scan and its system-change skip, and
  takes main's skip of rows the agent's view leaves out, dropping the
  tune-out check main dropped, so such a row is passed over rather than
  answering no.
- agent.ts: main's notice tag for a system injection's non-text block
  goes where this branch builds the system text, systemBlocks. That text
  is part of a head's system strings; no stored head holds the old
  wording, since the stored head record this branch adds is itself
  unreleased.
- The newest-entry tests: both kept, this branch's system changes after
  the entry and main's row the view leaves out.

Tests: a re-armed send refused for its system change resends once on
the head, and the resend answers the turn; a resend refused again ends
the turn with no third send, under a policy that would retry a refusal.
…st sends, and a change row is never said

A tool boundary injects what it stores into the live turn, and leaves out
a row the agent's own view leaves out, so the turn and the next compile
agree. It judged that by the metadata rule alone (sees), which a view
filter's content checks don't reach:
- the subconscious's view leaves out every system change not its own, so
  a host's change row written for it without metadata went into its live
  turn and out of every later request;
- on the head every change row stays out of the request, so a change row
  stored at a resident's tool boundary went into the live turn though no
  request sends it.
The boundary now asks the agent's own view filter, kept beside sees for
each context manager, and leaves out a system change for an agent that
doesn't declare entries.

A system change alters the request, not the conversation, so a change
row is never a message said to an agent, whatever its metadata: it
restarts no re-send count, counts as nothing heard, and moves no turn to
a channel. Before, a host's change row stored without metadata counted
as said, restarting a resident's count and the subconscious's.

The places that count what is said or heard (noteSpokenTo, noteArrival)
keep the metadata rule: they count said rows only, a change row is never
one, and today's view filters differ from the metadata rule only on
change rows, so it is exact for them. A view filter that leaves out a
conversational row would have to be weighed there too.

The type doc and the changelog fragments say a system change isn't a
message said to the agent, what a row the view leaves out does, and that
on the head no change row stored at a tool boundary is injected.

Tests: under 'entries' a host's unstamped change row at a resident's
tool boundary goes in and the next request sends it, and on the head
neither; at the subconscious's boundary it is stored but not injected,
and its next request leaves it out, its principal declaring entries or
on its head; such a row restarts neither a resident's re-send count nor
the subconscious's.
A change row stored while a provider hold keeps the turn's line held
leaves it held for the retry, and under 'entries' a change handed to the
round after a send leaves the retry's line held; a change row whose
metadata names a channel re-pins no turn.
The re-pin and held-write specs that call the changed helpers pass the
row.
…arried

Under 'entries' a system change stored at a tool boundary goes to the
live stream with the round's results, so the stream's next request
carries it, and membrane's check before sending reads it there too. The
fail-safe read only the stream's first request, so a refusal of that
later round went to the error policy, where a 400 is terminal, and the
resend on the head waited for a later compile carrying the stored
change. A stream a tool boundary hands a change now counts as one whose
request carried a change, so the refusal resends at once on the head.
The resend stays bounded: it compiles on the head, where a tool
boundary injects no change.

Tests: a turn whose first request carries no change, a host's change
stored at its tool boundary and the round after it refused, resends once
on the head without asking the error policy, and the turn ends well.
… prompt

A tool boundary that hands the live stream messages ends the tool loop
of a reply to [Continue], so the next round's rows carry no prompt. A
system change isn't a turn: the assembly reads a tool loop's prompt from
before the changes a request ends on, so a stream started again at that
point stamps the reply with the round's prompt, where the live boundary
dropped it. The boundary now drops the prompt only when something it
hands isn't a system change, so the live round and a compile at the same
point agree.

Tests: the round after a [Continue] whose tool boundary hands only a
host's change stores its reply with the round's prompt.
…it changes what goes ahead

A move of the head that leaves what the request carries ahead of its
messages as it was, such as a block of system text split at a line
break (a head's blocks join with one), leaves the identity digests as
they were, and the compile keeps the thinking, as it should, since the
request is the same. The binds doc, the identity doc and the fragment
said every move changes them.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

Replying to this review.

@slimepriestess main as it stood at e85c57db9a is merged into this pull request at 9d2f62310e, and four commits follow it, through 0150d3b453. Five changes at this head go past what you approved at d5bad56d, each with tests, and two things you read changed with the merge. The description is updated for this head. Line numbers below are this head's. Every run below was at this head, on Linux 6.12 x86_64 with Node 24.22.1, installed with no lockfile, with context-manager 0.23.1 and membrane 0.8.0; the receipts don't print chronicle's version.

Past what you approved:

  • The fail-safe in a re-armed turn, one that main's exhaustedTurnRearmMs (from #​846) sent again. The fail-safe decides before main's rule for such a turn (src/framework.ts:23582), 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. Two new tests in test/tool-declarations-failsafe.test.ts cover it: "resends a re-armed send refused for its system change once, at once, on the head, and the resend answers the turn" and "ends a re-armed turn whose resend is refused again, with no third send and no retry, even under a policy that would retry it".
  • The fail-safe for a later round handed a change (9e07b8bc0c). 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 now resends at once on the head, as a refusal of the first request does (src/framework.ts:14698). The new test "resends at once on the head when the refused request is a later round, given a host's change at a tool boundary" covers it.
  • A tool boundary injects into the live turn only what the next request sends (53954c3f20). It asks the agent's own view filter, and injects a change row only for an agent that declares entries (src/framework.ts:14387). 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. Two new tests in test/mid-turn-injection-as-compiled.test.ts cover it, each run with toolDeclarations at 'entries' and at 'head'.
  • A round handed only system changes keeps following [Continue] (8c9bfeff01), as a compile at that point reads it (src/framework.ts:14467). The new test in test/silent-heartbeat-rows.test.ts is "a later round handed only a system change at its tool boundary keeps following [Continue], as a compile there reads it".
  • A system change row is never a message said to the agent, whatever its metadata (isConversationalRow, src/framework.ts:444, in 53954c3f20). It starts no re-send count again under exhaustedTurnRearmMs, counts as nothing heard, moves no turn to a channel, and, handed to the live stream at a tool boundary, isn't new input to the turn. Tests for each are in test/exhausted-turn-rearm.test.ts, test/continuing-start-silence.test.ts and test/shared-channel-id.test.ts.

Taken out alone at this head and rebuilt, each of these parts fails a test or leaves one that never ends. The description's Tests section lists all 22 pieces we took out that way.

What changed in what you read:

  • The two view filters are now built on main's rule for what an agent's view holds, sees, which reads only metadata. The residents' filter is sees, which leaves out tune-out's rows and another agent's stored context injections, with !isOthersSystemChange beside it (src/framework.ts:13896 to :13899). The subconscious's is its own sees, which leaves out the residents' stored context injections, with !isSystemChangeNotOwn beside it (:13994 to :13995).
  • isNewestIn now also passes over a row the agent's view leaves out, such as a tune-out's, as main does (src/framework.ts:26964). With that skip taken out at this head, main's test "still newest with a row the agent's view leaves out after it, as a tune-out's" fails.

Your "not checked" item on kv-unified and the compressing strategies. With a test file of ours, not part of this pull request, we ran context-manager 0.23.1's PassthroughStrategy, AutobiographicalStrategy (flat, hierarchical, and with kv-unified folding) and KnowledgeStrategy, with a stand-in for membrane, at a context budget of 6,000 tokens. The description's "Under context-manager's strategies" gives the setup, what every request showed, and when the head moved. No real provider or summarizer took part, and no run was at real sizes.

The full suite at this head: 8,925 tests, 8,863 passing, 0 failing, 62 skipped.

Effie-2756 made the merge and the fixes.

— Fiona-2757

Effie-2756 added 7 commits October 28, 2026 15:30
0.29.0 folded changelog.d/838-rearm-exhausted-turns.changed.md into
CHANGELOG.md, and this branch had edited that fragment to add a system
change to what isn't a message said to the agent. The fragment is gone,
and that addition moves into this pull request's own bullet in
changelog.d/tool-declarations.added.md, which says a system change row
joins what 0.29.0's re-arm entry lists as not a message said to the agent.

src/framework.ts and src/types/agent.ts merged without conflict: main's
heartbeat drafts and this branch change different hunks. The
exhaustedTurnRearmMs doc in src/types/framework.ts is this branch's alone;
main changed neither it nor any test file this branch changes.
…at ends its instance, and the progress bounds

Main brings the agent registry (agent records, deriveAgent, reopening an
ended agent's context), 0.30.0, the change that refuses start() once a
stop() is admitted, and the progress bounds, whose error policy counts a
deadline membrane marks by its own tries. Git conflicts in src/agent.ts,
src/framework.ts and src/index.ts; each is resolved to keep both sides'
behaviour:
- the imports and exports of both;
- at the stream's error-policy decision, this branch's fail-safe still
  resends at once on the head, and otherwise the policy is asked with
  main's count of deadline tries, kept or cleared as main has it;
- the direct API's request filters out system changes and then presents the
  agent's own turns under its name; a request ends on the agent's own
  message by its presented name and this branch's context tail;
- a tool boundary releases landed mail, then notes the row as said, and
  injects a stored row only when the agent's exact view keeps it and, for a
  system change, when the agent declares entries;
- an arrival records whether it was addressed, from the row's metadata, and
  shared-slot readers come from the registry, each told of the whole row;
- a stored message releases landed mail before it is noted;
- the resident view filter keeps held rows out and another resident's
  change rows out;
- createEphemeralAgent takes the registry's spec second, as released in
  0.30.0, and headFrom moves to a third options argument;
- the recorded head's in-memory cache entry is dropped wherever an agent's
  runtime is released (releaseAgentRuntime), which every end now goes
  through, not only a conversation agent's disposal; the store keeps the
  head, which a later incarnation of the name reads.
Main's new view records (a reopened reader of the shared slot, a reopened
or live shared derivation) carry this branch's { sees, keeps } with keeps
read from sees, as main's behaviour; the next commit composes them.
… composed with its own

A context's view is now built once per kind of reader and composed for a
derived one, at every site: a resident's (held rows, another agent's stored
injections and another's change rows out), an observer's of the residents'
slot, such as the subconscious (another's stored injections and every change
row not its own out, held rows as its observe edge declares), and a derived
context's, which keeps what its source keeps plus its own stored injections
and its own change rows. The live shared derive and reopenAgentContext both
use the derived view, so a reopened fork shows what it showed live.

Before, the merge's composition read only the source's metadata predicate,
so a shared fork of a resident showed another resident's change rows, which
its source's view leaves out; and a fork reopened after it ended got no view
at all when its source read an isolated slot, such as the subconscious's, so
it showed a resident's stored injection its live compile left out
(agent-framework#887). Composing the source's view alone would hide the
fork's own change rows, which are stamped with its own name.

A reopened record's view follows what it declares: one that reads the
residents' slot through an observe edge gets the observer's view, with held
rows as the edge says; one that reads it as its own slot, the resident's.
A reopened subconscious therefore now gets its own view rather than the
residents'; the slot it is opened on is unchanged here.
…its head, by its spec's inherit

0.30.0's createEphemeralAgent takes the registry's spec second, and its
`inherit` is the released way to say a new agent continues another's
context. The new agent now takes its starting head from `spec.inherit.from`,
in either mode, when that agent is running and declares entries, so
deriveAgent's promise holds under toolDeclarations 'entries': with nothing
overridden, the child's first request is the parent's request plus its
framing, declaring nothing. deriveAgent's copy stamps the parent's own
system changes as the child's (systemChangeMetadata), as the conversation
fork's seed does; before, it re-added them with no metadata, so the child
would have declared them again.

The unreleased `headFrom` option, which named the agent by hand, is gone:
a host that copies messages itself passes a spec whose inherit names the
agent instead. An inherit naming no running agent gives nothing, so the new
agent adopts at its first request, where headFrom was refused.
…nts whose turns are its own

A shared child of an 'entries' parent reads the parent's change rows, which
are stamped with the parent's name, and starts on the parent's head; but it
counted only rows stamped with its own name as its own, so it declared the
parent's change again and its first request carried that change twice.
deriveAgent already presents the parent's turns as the child's own
(selfParticipants); a change row stamped by any of the agent's own
participants is now its own too, both where the head folds its own changes
and where a move of the head or a request on the head removes them.

A shared child's removal takes the parent's rows out of its own branch only:
the parent's stored rows and its compile stay as they were, at a request on
the head and at a move of the child's head. So a fork on the head, reopened
after its run, shows neither its own change rows nor its source's, which its
first request removed from its branch, and still not another resident's,
which its view leaves out.
…own change rows, the head seed's moment, a resumed job's head, and texts

- An agent's own change rows are those stamped, with change metadata
  ({kind: 'system-change', agent}), with one of its own participants (its
  name and selfParticipants). Its view now reads the same rule its head's
  folding and removal read, so an agent never folds or removes a row its view
  hides, and its own view hides none of its own: a resident's view leaves out
  a change stamped by anyone else, an observer's view keeps only its own, and
  a derived context keeps what its source keeps plus its own. A stamp is
  read only from change metadata, so a host's change row whose metadata
  happens to name an agent stays in every resident's view.
- The subconscious no longer copies the primary's selfParticipants or
  presentAs: they are identity, which createSubconsciousAgent overrides, so
  its own participants are its name alone and the primary's former-name turns
  read as another's, as its current ones do.
- An agent that continues another's context takes its starting head when its
  creation begins, before any await, so the head is never newer than the rows
  it continues: an older head costs one declaration, a newer one would carry
  a change twice. An inherit at a checkpoint the caller names (atSequence)
  gives no head, since the head at that point isn't recorded. The test that
  pinned the later read is turned round: a head the parent moves while the
  child's context opens costs the child one declaration.
- A resumed job gets a head store of its own in memory, as every ephemeral
  agent does, so it adopts at its first request, its head moves reach the
  trace and its operator alerts, and a child of it can start on its head.
- The addressed-elsewhere notice under the delivery locus reads the same rule
  as every other site: a system change row is never a message said, so it is
  never announced as someone speaking.
- Texts: selfParticipants' docs say it now also decides an agent's own change
  rows. Comments that gave reasons move here, under the house's comment rule:
  a change row is known by its content, and a resident's view leaves out
  another's since it declares another's tools; the fail-safe resends a
  refused request at once on the head, a retry of the same turn the error
  policy never sees (a 400 is terminal there), bounded since the resend
  carries no change, and a binding model resends too, at the cost a move of
  the head already has, with an operator line carrying the 400's text; a
  round handed only system changes follows the turn's prompt, since a change
  isn't a turn; the recorded head's state is one per agent, so a write covers
  its own; the direct API declares on the head with the caller's tools, so the
  context's changes stay out of it.
- Tests: each of these, plus a positive control for the re-pin test, the
  reopened subconscious fork's own injection and the residents' row, and a
  fork-view test's title that says what its reopen pins.
…quests use

A resident's live view counts a change row stamped with one of its
selfParticipants as its own, and so does a shared fork's view composed
from it. A fork reopened after it ends rebuilds its source's view from the
source's registry record, which for a resident carried no selfParticipants,
so the reopened fork left out a change stamped with the resident's former
name that it kept live. A resident's record now carries its configured
selfParticipants and presentAs, as a derived agent's record already does,
so a reopen rebuilds the view that ran, and inspect shows a resident's
mapping. A child whose own participants include those names, as
deriveAgent's shared child's do, took such a row as its own already; a
host's shared child (createEphemeralAgent with a shared inherit) doesn't,
and its reopen is what this pins.

Texts: createEphemeralAgent's doc and the changelog fragment say an inherit
at a checkpoint the caller names (atSequence) gives no head.
Effie-2756 added 3 commits October 29, 2026 02:56
…ain; a resumed view reads the mapping it resumes with

- What the request already carries can only take something out of what
  the agent declares. The difference is the agent's own, from its recorded
  head and its own changes, as before; then whatever the rest of the request
  carries identically (a tool defined the same, a tool already removed, a
  system block another row already adds) is left out of it. A host's shared
  child (createEphemeralAgent with a shared inherit and a config of its own)
  starts on its parent's head and keeps the parent's change rows through its
  source's view, but they aren't its own, so before it declared the parent's
  change again and its request carried it twice; a host's copy made without
  stamping did the same. Others' rows never cause a declaration or a move:
  counted into the declaration, a host's row adding system text the agent
  doesn't hold would read as a replacement, a move of the head on 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 it
  removed. Ownership still decides what a move removes and what a
  consolidation prices.
- resumeAgent reopens the context with the mapping the resumed agent runs
  with (an override's selfParticipants, else the record's), so its view and
  its head read one rule, as live.
- A conversation fork reads its template's head as its creation begins,
  before any await, as an agent that continues another's context does, so a
  head the template moves meanwhile costs the fork one declaration.
- Texts: createEphemeralAgent's doc and the changelog fragment say a change
  the request already carries isn't declared again and that a host copies once
  createEphemeralAgent returns; the fragment names an agent's own system
  changes, a resident's record's mapping and the subconscious's identity;
  selfParticipants' doc says the view keeps such a change in every mode;
  the boundary's comment says again that a round handed only system changes
  follows the prompt, which the guard below it does; a helper's doc comment
  goes.
- Tests: the unit test that pinned a host's row's change declared again
  is turned round; a host's shared child and a host's unstamped copy each
  carry the parent's change once; a host's row adding system text a resident doesn't
  hold causes no move and no declaration over two requests; a fork's head as its creation began; a resumed
  child's view under an override; the resident's record's mapping; the
  addressed-change test's premise that the change was stored.
…d changes with it

deriveAgent's copy stamps the parent's own system changes as the child's.
Since a change the request already carries is no longer declared again,
that stamp no longer shows at the child's first request; what it still does
is make the copied changes the child's own, so a move of the child's head
takes them out of its context instead of leaving them beside a head that
already holds them. This pins that.
…own window and thinking binding

Main brings agent-framework#869: the subconscious can take its own window
and thinking binding, checked as an agent's are. Git conflicts in
src/agent.ts and src/framework.ts, each resolved to keep both sides:
- the thinking binding's check is main's assertThinkingBinding, now shared
  by agents and the subconscious; its binds message keeps this branch's
  wording, since here leaving binds out declares neither and a choice that
  could set kept thinking aside is made as for a model that binds;
- the imports of both.
The subconscious's config keeps this branch's override of the primary's
selfParticipants and presentAs beside main's own window and binding fields.
main's new subconscious tests read the agent's thinkingBinds, which this
branch names declaresBinding (binds set true); they read it by that name.
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.

2 participants