Repository navigation
feat(agent): tell a resident when reasoning nothing in its window shows leaves its context - #826
Conversation
…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>
There was a problem hiding this comment.
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.bindsand thinking kept across stored injections. thinking-drops.ts already places provider drops and raises the operators'ThinkingLossIncidentsalert. 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
addMessagewith{ system: true, kind: ... }, for example 'routing-notice', 'prose-bounce', 'channel-notice' and 'auto-continue'. The newkind: 'reasoning-loss'notice sent withforAgentfollows that pattern, so it lands at the agent's next boundary like the others. (src/framework.ts (system notices withkind)) - New logic lives in small single-purpose modules, following how src/ is laid out.
reasoning-loss-notice.tsholds the wording as a pure function andreasoning-loss.tsholds 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
ThinkingLossIncidentsclass 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.thinkingBranchand an exportedbranchKey. 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'sthinkingStrip, 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:
- Medium severity. Per-agent told sets grow without bound for resident agents.
- Medium severity. Dropped stream rounds aren't recorded as told, so a later strip tells them again.
- Medium severity. Strip observer is only installed at the agent's first framework stream.
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 runsthinkingDrops.place(thinkingReport.dropped)once fornoteThinkingLossand again inside theforDropsclosure. 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 ascopeargument. 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.branchKeyused to carry a comment saying it is the key ofAgent.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.thinkingBranchandonThinkingStrip. 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. ThebindsanddropBlockdocs list every effect of the options, including thethinking-binding-lossops alert. They do not say that, with this change, the agent now also gets[reasoning]system notices of kindreasoning-lossin 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_CAUSESis typedPartial<Record<string, ReasoningLossCause>>rather than keyed by context-manager'sThinkingStrip['changed']element type. If context-manager renames a digest (for examplesystemtosystemText), 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 callingReasoningLossTellerdirectly with literal branch strings. No framework-level test checks thatStartStreamResult.thinkingBranchis plumbed intoStreamThinkingDrops.branch. Nor does one check that a stream on another branch (afterfork) starts a new incident, or thatisThisTurnmatches when the branch key comes fromprovenance.branchandcurrentBranchRef(). A regression wherethinkingBranchis 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,StoredReplyReaderandReasoningLossTurnare exported with no documentation. These rules are subtle: per-branch told sets, themessagesfold 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 asthinking-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.
| export class ReasoningLossTeller { | ||
| private readonly told = new Map<string, Map<string, Set<MessageId>>>(); | ||
| private readonly drops = new ThinkingLossIncidents(); |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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.
| 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())); | ||
| } |
There was a problem hiding this comment.
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>
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>
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>
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>
…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>
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
cause: 'head': the system text, the tools, the model, or the request settings), or at a branch's first compile (cause: 'first').RoundReport.thinking.dropped), placed byplaceThinkingDropsas 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
afterAnnotationsthrows away when an annotation began during it, orAgent.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
The line is stored as a system notice of kind
reasoning-loss, throughaddMessagewith 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.forStripturns a compile's strip into a line, with a told set per agent and branch.forDropsturns 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, whereaddMessagedefers the write rather than failing, except a host's directrunInference.src/thinking-drops.ts:ThinkingLossIncidents.observetakes 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 besideonCompiledToSend, 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 fromagent.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).driveStreamplaces 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:
trykeeps a host's failing observer from failing the compile, asonCompiledToSend's does.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.addMessagedropped the line.alerted, which part 3's operator-alert tests read.reportStripssupplies the strips a compile reports, since the tests don't enablebindsand so no real binding record strips anything; the teller tests'saidreads a line as the teller returns it;run'sbeforeRunandafterRunset 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 captureconsole.errorto 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 ownonThinkingStripbefore the first stream, which is what the framework'sif (!agent.onThinkingStrip)exists for.throughnames 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) andtest/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:
thinking-drops, and part 3'sthinking-loss-alert): 65 of 65.npm run build: 7,621 tests, 7,577 pass, 0 fail, 44 skipped.Not verified
ThinkingStrip's shape that the test supplies.What this leaves
AgentAPI is never told.runInferencein such a moment judges by the last turn's start, or calls none of the open turn's replies this turn's.[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.onThinkingStripstands in for the framework's, whose line for that agent then isn't told.changelog.d/reasoning-loss-line.added.mdWritten 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-seruaccount.