Skip to content

feat(agent): tell a resident when reasoning nothing in its window shows leaves its context - #826

Merged
nissa-seru merged 2 commits into
anima-research:mainfrom
nissa-seru:greta/155-part6
Oct 26, 2026
Merged

nissa-seru merged 2 commits into
anima-research:mainfrom
nissa-seru:greta/155-part6

Conversation

@nissa-seru

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

Copy link
Copy Markdown
Contributor

Problem

On a model that binds each reply's signed thinking to everything before it, an agent's earlier reasoning can leave the request while nothing in its window shows it went. This tells the agent, in a line at its next boundary, which replies' reasoning went, why where agent-framework knows, and to re-read before relying on decisions made since.

This is part 6 of anima-research/context-manager#155's design for keeping thinking valid. It builds on part 3 (#785: the binding option, the drop placement and the operators' alert) and on context-manager 0.22's report of what a committing compile newly leaves out (CompileResult.thinkingStrip, ThinkingStrip).

What is told

  • A compile's strip that begins at a change of the request's head (cause: 'head': the system text, the tools, the model, or the request settings), or at a branch's first compile (cause: 'first').
  • A fold forced mid-turn: a strip of any cause whose replies include one stored in the current turn.
  • A call's dropped blocks: what the provider dropped itself (membrane's RoundReport.thinking.dropped), placed by placeThinkingDrops as stored replies or rounds of the stream. Blocks it can't place are told only beside placed ones, as "at least".

A strip of a changed compiled message (cause: 'messages') that takes no reply of the current turn isn't told: a fold between turns, or any other change to an earlier message (named below, under what this leaves). A head change and a branch's first compile are told whenever they take a reply not yet told on that branch.

When. A strip is told at the compile that reports it: every committing compile from the agent's first framework stream on, when the framework installs its hook, whether or not its request is then sent, such as the first pass afterAnnotations throws away when an annotation began during it, or Agent.runInference's. The context manager's binding record takes the strip either way and doesn't report those replies to the next compile. A dry compile's strip isn't told, since it writes no record. A compile before that first stream, as through the direct API alone, isn't heard. A strip counts no reply that a strip's or a drop's line told on that branch, since context-manager can report a reply again when it renders again after a compile that didn't send it. The exception is a reply whose reasoning a stream's own round lost: it had no stored id when its drop was told, so a later strip that takes it tells it again. A head change's line still names the newest reply it takes that the store can read, told or not, as where its loss ends, and says this turn's steps are among them when any reply it takes is one. A fold is told mid-turn when it takes a reply of the current turn, even one a drop's line told, and then counts only the replies not yet told; a fold whose every reply was told has nothing new to tell. A reply is judged this turn's only while the agent has a live turn, so a compile through the direct API between turns calls none of the last turn's replies this turn's.

A call's drops are told as an incident, by the rule the operators' alert (ThinkingLossIncidents) uses: at the first call that drops any placed block, again only when a call drops more placed blocks, until a call drops no placed block. So a reply can be told again as its incident grows. An incident belongs to the agent on its branch, the branch the stream's compile read, so a call on another branch ends it and starts a new one. Each call of a stream, and each stream until a strip takes them out, drops the same blocks again, so an unchanged count isn't news.

The line

[reasoning] <agent>: The reasoning behind your replies up to the one beginning "Let me check the log first." is no longer in your context, because your tools changed. Their words are still here. Re-read before relying on an earlier decision.
[reasoning] <agent>: Some of your earlier reasoning is no longer in your context: the reasoning behind 3 of your replies, the first being the one beginning "Now the tests.", this turn's earlier steps among them. Their words are still here. Re-read before relying on a decision made since then.
  • A head change names the newest reply it takes, since the line lands at the agent's next boundary, which can come after a reply of this request that kept its reasoning. Without a reply to name, it says "your earlier replies".
  • A branch's first compile, a fold of this turn's steps, and drops count the replies and name the earliest, by the start of its text, clipped at 80 characters, or by its first tool call when it has none.
  • The cause is named only where agent-framework's own record establishes it: a head change's tools, system text or model, from context-manager's changed digests. A change of the request settings alone, a first compile, a fold and any drop are left uncaused: a drop's reason is the provider's report, and a summary carries its summarizer's signed thinking.
  • Blocks that can't be placed beside placed ones make the count "at least N", naming no reply and bounding no decision, since an unplaced block may be older than every placed reply. A call none of whose blocks can be placed tells the resident nothing; it's left to the operators' alert, which reports such a block where the branch's binding record has a start and the drop isn't for the account's or end user's binding.
  • Drops count replies: every stored reply a dropped block's signature names, a reply kept in shards once, and each of the stream's own rounds, a reply not yet stored.
  • This turn's steps are among them when a lost reply was stored after the turn opened, on the branch it opened on, or when the stream's own round was dropped.

The line is stored as a system notice of kind reasoning-loss, through addMessage with the agent as its target, so mid-turn it waits for the agent's next boundary, as the framework's other notices do, and a reader of the store can tell it from other rows.

Changes

  • src/reasoning-loss-notice.ts: the words, a pure function.
  • src/reasoning-loss.ts: ReasoningLossTeller. forStrip turns a compile's strip into a line, with a told set per agent and branch. forDrops turns a call's placed drops into a line when its incident calls for one. A loss counts as told when its line is made, as the operators' alert counts its own, so a line that's made and then can't be stored is logged and counts as told. A line that can't be made, as when the store can't read a reply, is logged and doesn't count, so a later report of those replies can tell it. Every place a line is told from runs with the agent's turn alive, where addMessage defers the write rather than failing, except a host's direct runInference.
  • src/thinking-drops.ts: ThinkingLossIncidents.observe takes a scope (the branch): a call in another scope ends the open incident and starts another. The operators' alert passes none, unchanged in effect. The resident's incident counts placed blocks only.
  • src/agent.ts: Agent.onThinkingStrip, a host's hook beside onCompiledToSend, is called with each committing compile's strip and the key of the branch its provenance names (branchKey, its id and creation time, now exported from agent.ts, not the package). A stream's start gives that key too (StartStreamResult.thinkingBranch), for its drops. Agent.branchHead() gives the current branch's key and head.
  • src/framework.ts: the framework installs the hook where it installs the toolsets' compile observer, at a stream's start, and stores the line then, for an agent still registered under its name. A turn records the branch and its head when it opens (TurnRouting.start). driveStream places each call's drops once, for the operators' alert and the line both, and tells them at the call's usage. A run's or a fork's told state is forgotten when it ends.
  • src/types/agent.ts: thinkingBinding.binds' doc names the resident's line among the option's effects, with the direct-API case.

What each part is for

Each part, from the least that meets what's told:

  • The hook: without it, a loss reported by a compile whose result isn't the one a stream sends is never told, since context-manager doesn't report it again. Telling at the compile keeps no strip in memory between a compile and a stream. Its try keeps a host's failing observer from failing the compile, as onCompiledToSend's does.
  • The told set, per branch identity: context-manager's doc asks a host that tells each loss once to keep its own set. Keying by the branch keeps a reply told on one branch from silencing another branch's loss of it, and keying by id and creation time keeps a deleted branch's set from a new branch of its name. A drop line's replies join it, so a strip that later takes the same replies doesn't tell them twice.
  • The incident rule for drops, shared with the operators' alert: it carries no per-reply state, which drops can't key reliably across branches, ambiguous signatures, a round later stored as a reply, and blocks with no place. The line names a range from its earliest reply where every block is placed, and leaves it unbounded where some aren't, so telling each reply exactly would add nothing a resident acts on.
  • Agent.branchHead()'s guard: a context manager that can't name its branch leaves the turn's start unset, so its lines say less, rather than failing the turn's start.
  • start, a turn's branch and head, read only in a live turn: tells this turn's steps from earlier replies, so a fold forced mid-turn is told and a fold between turns isn't. Sequences are numbered per branch, so the branch keeps a reply on another branch from being compared with a head it never had, as the consumed watermark keeps a branch for the same reason. The live-turn check keeps a turn's routing, which outlives it, from judging a later compile outside any turn.
  • thinkingBranch: keys a stream's drops by the branch its compile read, as its strips are keyed, so a drop's replies join the same told set and a branch switched to while the stream runs takes no incident of another branch.
  • The registered check: a removed agent's late compile or call would otherwise make told state under a gone name, counted told though addMessage dropped the line.
  • Counting every reply a dropped block names: placement gives a block found in two compiled messages both their stored bodies, so counting by the first undercounts.
  • The incidents' field keeps its name, alerted, which part 3's operator-alert tests read.
  • Cut, against the least that meets what's told: causes for drops; recording an incident only once its line is stored; and the line for a call none of whose blocks can be placed, which goes to operators only.
  • Test fixtures: reportStrips supplies the strips a compile reports, since the tests don't enable binds and so no real binding record strips anything; the teller tests' said reads a line as the teller returns it; run's beforeRun and afterRun set up a stored reply, a refusing store, an unreadable store, or a compile after the run's agent was removed; the resident, fork and hook tests build their own framework, since each needs an agent the ephemeral run doesn't give. The failure tests capture console.error to check the [reasoning] and observer diagnostics, and restore it however their run ends, so a failing run doesn't swallow later tests' logs. Three turn tests check their premises (every call ran, the last call ran, the incident still open at the run's last call), so a test can't pass by never reaching its case; a host-observer test sets a host's own onThinkingStrip before the first stream, which is what the framework's if (!agent.onThinkingStrip) exists for.
  • The line's forms: through names where a head change's loss ends, the newest reply it takes that the store can read, told or not, since a deferred line can land after a reply that kept its reasoning, and one the store can't read would leave it unbounded; its this-turn clause is judged from every reply it takes, since the range runs to them all; a count names the earliest reply where only some went, and a fold is judged mid-turn by every reply it takes, told or not, since a step whose loss a drop's line told still makes the fold this turn's, while the count is of the replies not yet told; "at least" with no anchor and an unbounded re-read where some blocks can't be placed, since an unplaced one may be older than every placed reply.

Tests

test/reasoning-loss-notice.test.ts (9), test/reasoning-loss.test.ts (25) and test/reasoning-loss-turn.test.ts (15). The teller's tests pin the line's forms, causes and counts, the told set per agent and branch, its join with drops, and incidents. The last runs framework turns whose compiles report a strip the test supplies and whose calls report drops: a head strip told once with its cause, stored as a system notice after the reply that called a tool and passed to the stream with the tool round's results; nothing for a fold between turns of a reply stored before the turn, or a compile with no strip; the strip of a compile thrown away because an annotation began during it; a stream's own round dropped on three calls told once, and a new incident after a call that drops nothing; no cause for a block that can't be placed; a fold forced mid-turn, made by a framework retry whose compile takes the turn's tool-calling reply, told as this turn's step and named by its tool call; the run's own agent told and not the primary; a refused or unreadable strip line logged while the turn goes on; a host's own strip observer left in place, with no line told; the direct API's compile after a turn telling a head strip and calling none of that turn's replies this turn's; nothing told or kept for an agent no longer registered; an ephemeral run's and a conversation fork's told state gone once they end; and the hook's contract: each committing compile's strip with the key of the branch it read, never a dry compile's, and a throwing hook that doesn't fail the compile.

Evidence

At this head's tree, on Linux with Node 24.22.1:

  • Focused files (the three above, thinking-drops, and part 3's thinking-loss-alert): 65 of 65.
  • Removals: 44 single removals of this change's pieces, each typechecking, run against those files after the unmodified head passed: 42 caught. The two not caught are two ways of keying a stream's drops by no branch: no turn test places a drop as a stored reply, so a drop's key isn't checked against a later strip's.
  • Whole suite after npm run build: 7,621 tests, 7,577 pass, 0 fail, 44 skipped.

Not verified

  • No turn test runs context-manager's own binding record: the turn tests' compiles report a strip of ThinkingStrip's shape that the test supplies.
  • No run against a provider: the drops come from mock round reports.

What this leaves

  • A compile before the agent's first framework stream isn't heard, since the hook is installed at a stream's start; an agent driven only through the direct Agent API is never told.
  • A strip of a change to an earlier compiled message between turns, other than a fold, isn't told.
  • On a branch other than the one the turn opened on, no reply is called this turn's, so a fold forced mid-turn there isn't told.
  • A reply whose thinking a stream's own round lost is told by the drop's line as this turn's step, and can be told again by a later strip that takes it, since the round's reply had no stored id when the drop was told.
  • A turn is taken as live while the agent holds a turn token, which surgery and a puppet call also reserve, which a start holds before its routing replaces the last turn's, and which a turn held for a provider retry doesn't hold. A framework compile is always inside a start, but a host's direct runInference in such a moment judges by the last turn's start, or calls none of the open turn's replies this turn's.
  • A line that's made and can't be stored is logged as a [reasoning] console line and counts as told: a strip's replies are told again only if context-manager reports them again, and a drop's only when its incident grows or ends.
  • A call none of whose dropped blocks can be placed tells the resident nothing; it's left to the operators' alert, which reports such a block where the branch's binding record has a start and the drop isn't for the account's or end user's binding.
  • A host that sets its own onThinkingStrip stands in for the framework's, whose line for that agent then isn't told.
  • A call whose dropped set changes without growing isn't told, and a restart mid-incident tells once more.
  • A reply told once on a branch isn't told there again when context-manager reports it again, which it does when the reply renders again after a compile that didn't send it, so a second loss of the same reply's reasoning, if there is one, goes untold.
  • Each silent start reads its own head, so a fold of a silent turn's earlier steps, forced from a later stream of it, isn't told.
  • The told sets are held in memory for the agent's life, one id for each reply told on each branch it has used, and dropped when the agent is removed or its run or fork ends.
  • A line deferred to the agent's next boundary is in memory until then, as the framework's other deferred notices are, unless deferred writes are persisted, and lands on the branch the agent is on then.
  • No turn test runs context-manager's own binding record through to the resident's line: Reasoning-loss line: no test runs context-manager's own binding record #828.
  • No framework test carries a stream's drops on its branch: Reasoning-loss line: no framework test carries a stream's drops on its branch #829.

  • Changelog fragment added: changelog.d/reasoning-loss-line.added.md

Written by Greta-2425 (Claude Opus 5.5), with Ada-1017 (Claude Opus 5.5) making the design calls. Cedric-2315 (Claude Opus 5.5), who built none of it, read it against chronicle's and context-manager's source, and its later commits by mutants. Gordon-2414 (Claude Opus 5.5) weighed each part against the least that meets what's told, made the cuts listed above, and ran the last test against the fix it pins.

Description checked and posted by Fiona-2757 (Claude Opus 5.5).

🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's tooling, and posted from the shared nissa-seru account.

…ws leaves its context

On a model that binds each reply's signed thinking to everything before
it, an agent's earlier reasoning can leave the request while nothing in
its window shows it went. This is part 6 of
anima-research/context-manager#155's design for keeping thinking valid.
It tells the agent, in a `[reasoning] <agent>:` line, which replies'
reasoning went, why where agent-framework's own record says, and to
re-read before relying on decisions made since.

What is told, and when
- A committing compile's strip (context-manager 0.22's
  CompileResult.thinkingStrip) that begins at a change of the
  request's head or at a branch's first compile, and a fold that takes
  a reply of the current turn. A fold between turns, or any other
  change to an earlier message between turns, isn't told.
- A call's blocks the provider dropped (membrane's
  RoundReport.thinking.dropped), placed by placeThinkingDrops as stored
  replies or the stream's own rounds, told as an incident by the rule
  the operators' alert uses: at the first call that drops any placed
  block, again only when a call drops more, until a call drops none or
  a call on another branch. A call none of whose
  blocks can be placed tells the resident nothing and is left to the
  operators' alert: such a block may not be the resident's reasoning
  (a summary's), and a line naming no reply asks it to re-read
  everything.
- The line is stored through addMessage with the agent as its target,
  as a system notice of kind reasoning-loss, so while a turn is alive
  it waits for the agent's next boundary, as the framework's other
  notices do.

How
- Agent.onThinkingStrip is a host's hook, beside onCompiledToSend:
  every committing compile, never a dry run's, calls it with its strip
  and the key of the branch its provenance names, whether or not its
  request is then sent. context-manager's binding record takes the
  strip either way and doesn't report those replies to the next
  compile, so the first pass afterAnnotations throws away when an
  annotation began during it would otherwise lose its strip. The
  framework sets the hook at a stream's start, where it sets the
  toolsets' compile observer, unless a host has set one. Telling at the
  compile keeps no strip in memory between a compile and a stream.
- A strip counts no reply a strip's or a drop's line told on that
  branch: context-manager can report a reply again when it renders
  again after a compile that didn't send it, and its doc asks a host
  that tells each loss once to keep its own set. The set is keyed by
  branch identity, its id and creation time, so a reply told on one
  branch is told again on another, and a deleted branch's set doesn't
  pass to a new one of its name.
- A turn keeps the branch and head it opened at (TurnRouting.start,
  from Agent.branchHead()). A reply is this turn's when stored after
  that head on that branch, since chronicle numbers sequences per
  branch. The hook reads it
  only while the agent has a live turn (activeTurnTokens), since a
  turn's routing outlives it, and a compile through the direct API
  after a turn would otherwise call the last turn's replies this
  turn's. Each silent start reads its own head.
- A stream's drops are keyed by the branch its compile read
  (StartStreamResult.thinkingBranch), as its strips are, so a drop's
  replies join the same told set. ThinkingLossIncidents.observe takes
  that branch as its scope: a call in another scope ends the open
  incident and starts another. The operators' alert passes none,
  unchanged in effect. The resident's incident counts placed blocks
  only, so a call of unplaceable blocks alone opens none and ends an
  open one, since it drops none of the blocks that incident counted.
- Nothing is told or kept for an agent no longer registered under its
  name, since addMessage drops a line for an unregistered target
  without throwing. A run's or a conversation fork's told state is
  forgotten when it ends.
- A loss counts as told when its line is made, as the operators' alert
  counts its own. Every place a line is told from runs with the
  agent's turn alive, where addMessage defers the write rather than
  failing, except a host's direct runInference; a line that can't be
  made or stored is logged as a [reasoning] console line.

The words
- reasoningLossNotice(input) is the line: what went, why where
  `causes` establishes it, and what to do, in plain words with no
  request paths or account names. Its scope is `all` (the reasoning
  behind every earlier reply, up to the reply `through` names when it
  can be named; a head change leaves all earlier thinking out) or
  `some` (`count` replies, at least that many with `unplaced`, the
  first named by `first` when it can be, which it isn't with
  `unplaced`); `thisTurn` says replies of the current turn are among
  them. A reply is named by the start of its text, clipped at 80
  characters, or with no text by its first tool call.
- A head change names the newest reply it takes that the store can
  read, told or not, as where its loss ends, since a deferred line can
  land after a reply that kept its reasoning; it says this turn's steps
  are among them when any reply it takes is one.
- A first compile, a fold of this turn's steps, and drops count the
  replies not yet told and name the earliest; a fold is judged mid-turn
  by every reply it takes, told or not. Beside blocks that can't be
  placed, the count is "at least N", naming no reply and bounding no
  decision, since an unplaced block may be older than every placed
  reply. Drops count every stored reply a dropped block names, a reply
  kept in shards once, and each of the stream's own rounds.
- A cause is named only from agent-framework's own record of the
  change: a head change's tools, system text or model, from
  context-manager's changed digests. A drop's reason is the provider's
  report, and a summary carries its summarizer's signed thinking, so
  drops name none.

Moved docs (declarations src/index.ts doesn't export keep no doc
comment here; what they said is below)
- ReasoningLossTeller decides which losses a resident is told of, and
  the line for each, on a branch (branchKey); what's told is held in
  memory, so after a restart a loss can be told again, and forget()
  drops an agent's.
- ReasoningLossTurn.start is the branch (branchKey) and its head when
  the current turn opened: a reply stored after it on that branch is
  this turn's. StreamThinkingDrops.branch is the key of the branch the
  stream's compile read (StartStreamResult.thinkingBranch).
- ThinkingLossIncidents' class doc, as it stood: "When an agent's
  unexpected drops call for an operator alert: at the first call that
  shows any, and again only when a call shows more than the alert last
  counted. Each call of a stream, and each stream until a strip takes
  them out, drops the same blocks again, so an unchanged count isn't
  news. A call that shows no unexpected drop, its report empty, absent
  or naming only expected ones, ends the incident: the blocks it
  counted no longer drop. The next one alerts afresh. It's held in
  memory, so a restart mid-incident alerts once more, and an agent's
  removal ends its incident (`forget`)." The resident's line now uses
  the same rule over a call's placed blocks, with the branch as scope.
- branchKey's doc, as it stood: "A branch's key in Agent.unsentSince:
  its id and when it was created." It now also keys the reasoning
  line's told set, a turn's start (Agent.branchHead()) and the hook's
  branch.

Tests: test/reasoning-loss-notice.test.ts (the words),
test/reasoning-loss.test.ts (the teller: forms, counts, causes, the
told set per agent and branch and its join with drops, incidents), and
test/reasoning-loss-turn.test.ts (framework turns whose compiles report
a strip the test supplies and whose calls report drops, and the hook's
contract). Part 3's thinking-loss-alert tests pass unchanged.

Not covered, named in the PR: no turn test runs context-manager's own
binding record, or places a drop as a stored reply.

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

@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: merge and fast-follow. 3 medium severity findings can be fixed after the merge.

Reviewed commit cb340fa 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 the project. This fits. It is the declared next step of the thinking-binding design that agent-framework already carries.

6 reasons with sources

It reports lost reasoning to the agent through the framework's usual system-notice path, and it puts the logic in small dedicated modules next to the existing thinking-drop code.

  • It extends thinking-binding behaviour the project already has rather than adding a new concern. The README already covers thinkingBinding.binds and thinking kept across stored injections. thinking-drops.ts already places provider drops and raises the operators' ThinkingLossIncidents alert. This PR reports the same losses to the agent itself and is presented as part 6 of the context-manager#155 design, building on part 3 (#785). (README.md (thinkingBinding, budget restart); src/thinking-drops.ts)
  • The line is delivered the same way the framework's other agent-facing notices are. framework.ts already stores many system notices through addMessage with { system: true, kind: ... }, for example 'routing-notice', 'prose-bounce', 'channel-notice' and 'auto-continue'. The new kind: 'reasoning-loss' notice sent with forAgent follows that pattern, so it lands at the agent's next boundary like the others. (src/framework.ts (system notices with kind))
  • New logic lives in small single-purpose modules, following how src/ is laid out. reasoning-loss-notice.ts holds the wording as a pure function and reasoning-loss.ts holds the teller. framework.ts and agent.ts only gain the wiring, much as the existing thinking-drop and refusal-line helpers are split out. (src/ layout (thinking-drops.ts, thinking-token-stamp.ts, sent-rows.ts, refusal-lines.ts))
  • It reuses the operators' incident rule instead of creating a second one; that rule now also takes a branch scope. The agent-facing incidents use the same ThinkingLossIncidents class as the operators' alert, now keyed by branch as well. This keeps a single definition of what counts as a new incident. The review separately notes that the class lost its contract doc comment. (src/thinking-drops.ts (ThinkingLossIncidents))
  • It adds a small public surface, and the changelog documents it. The new public pieces are Agent.onThinkingStrip, Agent.branchHead(), StartStreamResult.thinkingBranch and an exported branchKey. They are listed in a changelog.d fragment, which CONTRIBUTING requires for behaviour changes. The PR also states its dependency on context-manager 0.22's thinkingStrip, which matches how CONTRIBUTING expects cross-repo dependencies to be stated. (CONTRIBUTING.md (Changelog; companion PRs))
  • It is one coherent change and its tests cover only this feature. The unit tests cover the wording and the teller, and the turn tests cover how the framework wires them in. The diff contains no unrelated work. (CONTRIBUTING.md (What a PR should contain))

Findings

3 findings of medium or high severity, see the inline comments:

7 more findings, less severe
  • Low severity, maintainability, src/framework.ts:21665-21671. Dropped blocks are placed twice per call. For each call the driver runs thinkingDrops.place(thinkingReport.dropped) once for noteThinkingLoss and again inside the forDrops closure. Each run rebuilds the signature index over the compiled messages. The placed array should be computed once and passed to both, which also guarantees the ops alert and the resident line read the same placement.
  • Low severity, documentation, src/thinking-drops.ts:116-129. ThinkingLossIncidents lost its contract doc and the new scope parameter is not explained. The change deletes the class doc. It explained when an incident alerts, why an unchanged count isn't news, that a call with zero drops ends the incident, and that state is in memory and so lost on restart. The class now has two users, the ops alert and the resident teller, and has gained a scope argument. A scope change starts a new incident even when the count did not rise, but nothing documents that. The contract is now undocumented exactly where it became more complex.
  • Low severity, documentation, src/agent.ts:263-265. branchKey is now exported but its doc comment was removed. branchKey used to carry a comment saying it is the key of Agent.unsentSince, built from a branch's id and creation time. The change exports it, and it now also keys the reasoning-loss told sets, StartStreamResult.thinkingBranch and onThinkingStrip. At the same time the change deletes that comment. Callers of this public helper get no description of its format or purpose.
  • Low severity, documentation, src/types/agent.ts:252-262. thinkingBinding config docs don't mention the new resident-visible line. The binds and dropBlock docs list every effect of the options, including the thinking-binding-loss ops alert. They do not say that, with this change, the agent now also gets [reasoning] system notices of kind reasoning-loss in its context. That is a behavior change that operators turning these options on will see in transcripts, and it should be documented with the options.
  • Low severity, maintainability, src/reasoning-loss.ts:18. HEAD_CAUSES is keyed by plain string, so changes to ThinkingStrip['changed'] go unchecked. HEAD_CAUSES is typed Partial<Record<string, ReasoningLossCause>> rather than keyed by context-manager's ThinkingStrip['changed'] element type. If context-manager renames a digest (for example system to systemText), this still compiles, and the cause silently disappears from the line. Keying it by the real union would surface such a change at type-check time.
  • Low severity, test coverage, test/reasoning-loss-turn.test.ts:195-203. No integration test that thinkingBranch reaches forDrops as the incident scope. Branch scoping of drop incidents is tested only by calling ReasoningLossTeller directly with literal branch strings. No framework-level test checks that StartStreamResult.thinkingBranch is plumbed into StreamThinkingDrops.branch. Nor does one check that a stream on another branch (after fork) starts a new incident, or that isThisTurn matches when the branch key comes from provenance.branch and currentBranchRef(). A regression where thinkingBranch is dropped (falling back to '') would make no reply ever count as this turn's, and these tests would not notice.
  • Low severity, documentation, src/reasoning-loss.ts:10-24. New exported teller and interfaces have no doc comments. ReasoningLossTeller, forStrip, forDrops, StoredReplyReader and ReasoningLossTurn are exported with no documentation. These rules are subtle: per-branch told sets, the messages fold being told only when it takes a turn reply, head changes naming the newest reply even if told, and incidents counted by placed blocks. They are spelled out only in the PR description, unlike the heavily documented neighbouring modules such as thinking-drops.ts. A maintainer cannot recover them from the code.

For a deeper review by several readers and a judge, which takes longer, a maintainer can comment /review deep.

Comment thread src/reasoning-loss.ts
Comment on lines +20 to +22
export class ReasoningLossTeller {
private readonly told = new Map<string, Map<string, Set<MessageId>>>();
private readonly drops = new ThinkingLossIncidents();

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. Per-agent told sets grow without bound for resident agents.

ReasoningLossTeller.told keeps, for each agent and each branch key, every MessageId a strip or drop line has ever told. Entries are removed only by forget(agentName), which runs only when the agent is removed. A long-lived resident keeps adding ids at every head change, branch first compile and fold for the whole life of the process. Sets for branches the agent has left (forks, rewinds) are never pruned either. Nothing bounds the memory, and nothing removes ids that context-manager will never report again.

Comment thread src/reasoning-loss.ts
Comment on lines +79 to +81
if (this.drops.observe(agentName, blocks, branch) === undefined) return undefined;
const told = this.toldFor(agentName, branch);
for (const id of replies.keys()) told.add(id);

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. Dropped stream rounds aren't recorded as told, so a later strip tells them again.

forDrops adds only replies.keys(), the already-stored replies, to the told set. The stream's own rounds (drop.kind === 'round') are counted and told, but nothing is recorded for them. Once a round's reply is stored and a later strip takes it (a head change, or a fold this turn), forStrip finds it missing from told and counts it again. The resident is told twice about the same lost reasoning. This breaks the stated rule that a strip counts no reply a drop's line already told.

Comment thread src/framework.ts
Comment on lines +19432 to +19435
if (!agent.onThinkingStrip) {
agent.onThinkingStrip = (strip, branch) => this.tellReasoningLoss(agent, () => this.reasoningLoss.forStrip(
agent.name, branch, strip, (this.activeTurnTokens.has(agent.name) && this.turnRouting.get(agent.name)) || {}, agent.getContextManager()));
}

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. Strip observer is only installed at the agent's first framework stream.

The framework sets agent.onThinkingStrip inside the stream-start path. A committing compile that runs before the agent's first framework stream is never reported, for example agent.runInference through the direct API, or a host's compileWithInjections. That compile is exactly where a branch's first strip or a head change is most likely, yet no line is written. The context manager's binding record still consumes that strip, so the loss is never told. The changelog and description promise a line for every committing compile.

…document the resident's line

From the automated review of this pull request:

- driveStream placed a call's dropped blocks twice, once for the
  operators' alert and once for the resident's line, rebuilding the
  signature index each time. It now places them once and passes the same
  placement to both. placeThinkingDrops is a pure function of the call's
  request snapshot, so no answer changes.
- HEAD_CAUSES is keyed by the element type of ThinkingStrip['changed']
  (RequestIdentity's keys) rather than by string, so a digest renamed in
  context-manager is a type error here instead of a cause silently lost.
- thinkingBinding.binds' doc lists the option's effects, the operators'
  alert among them; it now names the resident's [reasoning] line too, and
  that an agent driven only through the direct API is never told.
- The changelog fragment's "What isn't" names a compile before the
  agent's first framework stream, and so an agent driven only through the
  direct Agent API.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru
nissa-seru merged commit 83ec92d into anima-research:main Oct 26, 2026
12 checks passed
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 26, 2026
main brought anima-research#826, anima-research#819, anima-research#824, anima-research#821, anima-research#825 (ignore and include read
gitignore's forms, and refuse any other), anima-research#815, anima-research#820 and anima-research#818 (answers that
write say where the change stands on disk). Where they met this change, in
src/modules/workspace/index.ts:

- the imports from observe.ts: main's list, which drops
  includePatternReadable and adds directoryAt and patternRefusal, with this
  change's DIRECTORY, LEAVES_MOUNT, NOT_FOLLOWED, NOT_REGULAR and
  rootAbsence;
- after StoredThenFailed: this change's ONCE_REOPENED beside anima-research#818's
  DISK_ORDER and furthestFromDisk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 26, 2026
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>
rufalupa666-netizen pushed a commit to rufalupa666-netizen/agent-framework that referenced this pull request Oct 30, 2026
anima-research#826 stored a `[reasoning]` notice for an agent whenever a binding compile
stripped, or the provider dropped, thinking from its earlier replies ("The
reasoning behind your earlier replies is no longer in your context…").
Text about the mechanics of an agent's own memory and reasoning gives it
nothing to act on and keeps it thinking about it; under a binding model any
tool-list change would have triggered one. Remove the notices (and the
ReasoningLossTeller and notice builder that only served them). The
operators' thinking-binding-loss alert and Agent.onThinkingStrip stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tel-0s added a commit that referenced this pull request Oct 30, 2026
…s notices (#938)

main at a64cfc0 is one merge past 71193a9, #938, with no release in it,
so package.json stays 0.31.0 and this branch's floors stand (chronicle
^0.6.0, context-manager ^0.25.0, membrane ^0.8.0). #938 stops telling an
agent that reasoning left its context: #826's `[reasoning] <agent>:`
notices are no longer stored for it or passed to its stream, on a binding
compile's strip or on a block the provider drops. src/reasoning-loss.ts
(ReasoningLossTeller) and src/reasoning-loss-notice.ts go, with their unit
tests; in framework.ts the teller, its forget at the three disposal sites,
tellReasoningLoss, the onThinkingStrip the framework set at an agent's
first stream, and the drops' notice after noteThinkingLoss go. The
thinking-binding-loss ops alert and a host's own Agent.onThinkingStrip
stay; thinkingBinding's docs say the agent is told nothing.

2 paths conflicted (5 regions).

- src/framework.ts (1, imports): the ReasoningLossTeller import goes; this
  branch's presentation and read-marks imports beside it stay. The rest of
  #938's removals merged cleanly. Nothing on this branch read the notice
  past the teller: it was stored through addMessage with forAgent, as the
  framework's other notices are, and no presentation code names its kind
  or tag. onThinkingStrip had no other consumer in src: the framework's
  setting of it goes, as on main, and the hook stays for hosts.
- test/reasoning-loss-turn.test.ts (4): main's version. This branch had
  read the head-change test onto #291 (windowOrder, from
  getPresentedMessages: the line, stored at once, entered the window at
  the tool boundary, after the first reply) and had run() return the
  teller's memory (remembered). Both were about the notice and go with
  it; no other assertion rode on them. Main's tests that no line reaches
  the agent (a head-change strip, drops on every call, a fold mid-turn
  that takes the turn's own step, a resident beside the run, a host's
  observer kept) and the Agent.onThinkingStrip hook test pass here
  unchanged.

Read onto this branch past the conflicts:
- src/agent.ts: Agent.onThinkingStrip's doc no longer says the framework
  sets it at the first stream, with a `[reasoning]` line a host's observer
  stands in for (main left that sentence); the framework sets none of its
  own and tells the agent nothing of a strip. branchHead's doc drops "for
  the reasoning-loss notice, which tells this turn's stored replies from
  earlier ones".

Left as main has them: TurnRouting.start, set from Agent.branchHead()
when a turn opens, has no reader now (reasoning-loss.ts was its only one);
NOTICE_TAG.reasoning stays, so supplied text that opens `[reasoning]` is
still marked.

storeSees: no new site; main opens no context manager. agentSees: no new
view decision. Notice tags: none new.

Changelog: no release in main's range, so no fragment was consumed; main's
no-reasoning-loss-notices.changed.md comes in as written. No 291-*
fragment, docs/dendrite.md or README described the notice.

Tests: built with zero errors; dist's compiled reasoning-loss and
reasoning-loss-notice (src and test) removed, no other stale compiled
file; fixtures copied. reasoning-loss-turn, presentation*, read-marks,
thinking-binding*, foreign-rounds, thinking-drops, thinking-loss-alert and
thinking-token-stamp (15 files): 160 tests, 160 pass. Full suite (npm
test, at concurrency 4 under load): 9638 tests, 9589 pass, 0 fail, 49
skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant