Skip to content

feat(agent): thinkingBinding.dropBlock asks the provider to drop thinking blocks that a fold invalidated - #768

Merged
nissa-seru merged 2 commits into
anima-research:mainfrom
nissa-seru:feat/thinking-binding-drop-block
Oct 23, 2026
Merged

nissa-seru merged 2 commits into
anima-research:mainfrom
nissa-seru:feat/thinking-binding-drop-block

Conversation

@nissa-seru

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

Copy link
Copy Markdown
Contributor

This adds thinkingBinding.dropBlock to an agent's config. Set, it asks the provider to drop a signed thinking block whose earlier conversation has changed, as a fold changes it, rather than refuse the request. On a framework turn, each call's report of what the provider dropped goes to the inference:usage trace, and neither a call that dropped blocks nor its stream's later calls feed the context strategy's calibration. It's off by default, and an agent without it sends nothing new.

Problem

A thinking block's signature binds it to the conversation before it. When the context manager folds, it rewrites that earlier conversation, so every block after the change is bound to a conversation the request no longer holds. An account that enforces the binding answers such a request with a 400, "Invalid signature in thinking block. The block is bound to a different conversation." That happens on every turn after the agent's first fold (anima-research/context-manager#155).

The provider's control for it is thinking.block_binding.prefix_mismatch_behavior: "drop_block", with the thinking-binding-controls-2026-08-01 beta. The provider then drops the blocks that no longer match, doesn't bill them, and answers, keeping every block that still matches. It reports what it dropped in input_transformations. membrane 0.6.0 carries the control as config.thinking.blockBinding through its Anthropic and Bedrock adapters, and reports each round's entries as RoundReport.thinking (antra-tess/membrane#108). On main, agent-framework never sets it.

Accounts differ. Four accounts tried on 2026-10-18 got neither the beta nor the control, which is what agent-framework sends today. On Opus 5.5 all four, and on Fable 5.1 the two that tried it, dropped those blocks themselves, without saying so, and each billed the same input as the same request under drop_block. "error" answered 400 on all four. anima-research/context-manager#155 reports the 400 on accounts created on or after 2026-08-31. An account can also show a block that failed the check to the model, which membrane reports as mismatchAllowed; none of the four did.

Changes

  • The setting: AgentConfig.thinkingBinding.dropBlock, a boolean, off by default (src/types/agent.ts:179).
  • The request: when it's true, requestSettings() adds blockBinding: { prefixMismatchBehavior: 'drop_block' } to the thinking config the agent sends (src/agent.ts:1546). A framework turn and the direct API both take their config from there. An agent whose thinking config is left out, null or false sends { enabled: false, blockBinding }. membrane carries that to a model whose thinking is always on (Fable, Mythos) as an adaptive config, and sends nothing from it to any other model, since thinking is off there.
  • The report: on a framework turn, a call whose RoundReport.thinking names a block gets it in its inference:usage trace as thinking: { dropped, mismatchAllowed } (src/framework.ts:20548, src/types/trace.ts:207). That covers blocks the provider dropped, and blocks it showed the model though they failed the check. The direct API carries the control but reads no report, since it traces no usage and feeds no calibration.
  • Calibration: once a call of a stream reports dropped blocks, neither it nor any later call of that stream is passed to the strategy's reportRealInputTokens (src/framework.ts:18831, :20515). The provider doesn't bill a block it dropped, while the strategy still counts it, so such a call's ratio would pull the calibration low. The strategy takes the first call it's given after a compile as that compile's one sample, and a stream's later calls carry what the stream appended, so skipping only the call that dropped blocks would hand the sample to a grown request.
  • The refusal: assertAgentSwitches refuses a thinkingBinding that isn't an object, null or false, and a dropBlock that isn't true or false (src/agent.ts:128), with null counting as left out, as for the other switches.
  • Text: the setting's doc, a clause in thinking's and display's on models whose thinking is always on, the trace field's doc, and changelog.d/thinking-binding-drop-block.added.md.

Who needs to act

  • A host that wants the control sets thinkingBinding.dropBlock for agents whose route and model take it.
    • The route: membrane's Anthropic or Bedrock adapter. Through any other adapter the setting does nothing, so the host is the place to refuse it there, since it builds the Membrane and knows the route.
    • The model: Opus 5.5, Haiku 5.5, Sonnet 5.5, Sonnet 5, Fable 5 and Fable 5.1 accepted the config on 2026-10-18. Mythos hasn't been tried, and membrane notes that the family has refused thinking configs with a 400 before, so for a Mythos model the setting may fail every request.
    • Telling the kinds apart: membrane tells a model whose thinking is always on by its id, which names fable or mythos. One behind another id, such as a router's alias, carries the control only while the agent's thinking.enabled is on.
  • On a model whose thinking is always on, the thinking config that carries the control is the only one membrane sends, and it carries the agent's thinking.display. So an agent there with display: 'summarized' gets its thinking text back only with the setting. membrane 0.5.87 sent such a model no thinking config either.
  • A host that sends a thinking config of its own, through request.extra or as thinking in providerParams, replaces the one membrane builds, control included: the Anthropic adapter applies extra over its params, and membrane's doc for blockBinding says the same of providerParams. connectome-host's logging adapter does so for an agent with reasoning on (withReasoning, src/logging-adapter.ts). Such a host carries block_binding in its own config for the control to reach the provider. The provider's report on such a call is traced, and kept out of calibration, the same way, whoever asked for the control.
  • A subconscious (create()'s subconscious) inherits the setting from the agent it serves, with the rest of that agent's inference settings, thinking included (src/framework.ts:11821-11842). It does so even when subconscious.model names another model, so that model should take the control too.

What each part is for

  • The setting is the only way to ask for the control per agent. It's off by default, with no model list in the framework, since the route and the model are the host's to know, and a host sets it from its own model records.
  • A boolean, not the provider's enum. "error" would turn an account's silent drop into a 400, which no agent asks for.
  • Its own field, not inside thinking, since thinking requires enabled, and a model whose thinking is always on takes the control whatever enabled says.
  • The { enabled: false } config for an agent with none lets such a model carry the control. For any other model membrane sends nothing from it.
  • thinkingDropBlock is private to the agent, read only by its request settings. A host sets the config field, and nothing outside the agent needs it back.
  • The trace makes each drop visible without an alert. After a fold, the same blocks are dropped on every call until they leave the context, so an alert would fire on routine folds.
  • The calibration skip keeps the strategy from learning a low ratio from calls billed for less than they held, and the rest of the stream from handing it a grown request in their place. A compile whose first call dropped blocks gives no sample, and the calibration keeps its value. That's the ordinary case after a fold: each block after the stretch the fold rewrote is bound to a conversation the request no longer holds, and is dropped on every call until a later fold takes it out of the context, so for as long as such blocks stay, no stream gives a sample. If the provider drops an earlier model's blocks after a model switch (model_binding_mismatch), they drop on every call until they leave the context, and the calibration takes up the new model's tokenizer only after that.
  • The refusal holds the rule every boolean switch here follows, so a quoted "false" doesn't turn it on.
  • Left out:
    • Refusing the setting on an adapter that can't carry it. The framework gets one Membrane from the host, whose adapter isn't exposed, so it can't see the route at config load. The host can.
    • Counting dropped blocks in the two checks that read a stream's billed input: the maxStreamTokens restart (agent.lastStreamInputTokens, src/framework.ts:12246), which checks the stream's accumulated fresh input, and the in-stream projection (agent.lastStreamRealInputTokens, src/framework.ts:20470), which checks the next round against physicalWindowTokens. A dropped block isn't billed, so the restart comes later by its size, and the projection runs low by it whenever the provider counts it toward the window, which hasn't been measured. On the accounts above, main already bills that way. With the setting, the two differ only on an account that enforces, where they stand in for a 400.
    • A report on the direct API. It reads responses through membrane's non-yielding stream, traces no usage and feeds no calibration, so there's nothing there for a report to change.
    • The context manager's own compression and merge requests, which it builds and sends itself. They don't carry the setting. Folding breaks preserved-thinking signatures on enforced accounts: resident 400s ("bound to a different conversation") on every turn after the first fold context-manager#155 describes how it retries them without carriers.

Tests

At this head, e6a59222, on main e66bdae1, on Linux with Node 24.22.1, from a clean copy of each commit, installed as CI does: membrane 0.7.1 and context-manager 0.20.0, one copy of membrane.

  • Typecheck and build are clean at each of the two commits.
  • The full suite at this head: 6,721 tests, 6,677 passing, 0 failing, 44 skipped.
  • The new file, test/thinking-binding.test.ts, passes, 4 of 4. It pins:
    • the thinking config the agent sends, with and without the setting, for an agent whose thinking config is left out, null or false, and with the agent's own config left as it was;
    • what reaches the wire through membrane's real Anthropic adapter, against a local stand-in for the Messages API: block_binding with the beta for a model with thinking on, an adaptive config for Fable 5.1 with no thinking config, and nothing for another model with thinking off or without the setting;
    • a four-call stream: a call with no report, one whose report shows a block, one that dropped a block and one whose report names nothing. Each call's inference:usage trace carries what its report names, and calibration takes the two calls before the drop;
    • the refusals from create() and new Agent(), with null and false counting as left out.
  • agent-cache-ttl's direct-API tests now set the control, so the direct API's request carries it, and carries what a framework turn's does.
  • So do the neighbouring files: config-switches 8, agent-cache-ttl 7, end-turn-usage 41, activation-request-sanitize 5, assistant-persist-refusal 5, kv-unified-wiring 3, unconvertible-tool-result 11, unsent-compiles 5 and tool-result-guard 150.
  • Red-first: with src/agent.ts and src/framework.ts from main at e66bdae1 and this head's types and tests, all four new tests fail. With them from the first commit, 19c87e4f, the four-call stream test fails, on the calibration taken after the drop.
  • Each piece, taken out alone, fails a test, and the head passes again after:
    • the control left out of the config;
    • no config for an agent with none, or a null thinking config passed on as one;
    • the control written into the agent's own config;
    • a call that dropped blocks still calibrating, or only that call skipped and the rest of its stream given;
    • a call with no report read as one that dropped blocks;
    • no report in the trace, the report traced when it names nothing, or shown blocks left out of it;
    • the direct API sending the agent's thinking without the control (agent-cache-ttl);
    • no refusal.
  • Where this meets main: main passes { system, tools } with each calibration sample (fix(calibration): the sample carries the request's system prompt and tools (context-manager #323) #763, for Calibration learns tool-schema and system-prompt overhead as a per-token ratio; at 1.00 it rejects every sample until one dips under 1.8, then climbs to the clamp context-manager#323), and that call sits inside this change's skip. Each, taken out alone, fails its own test: the skip fails the four-call stream test, and the second argument fails calibration-request-overhead's.
  • dropBlock is read as === true after the refusal, which any non-boolean meets first, so no test can tell that read from a truthy one.
  • test/helpers/mock-membrane.ts passes a response's test-only round into its usage event, as membrane's yielding streams send it.

Not verified

  • No call reached a provider through this change. The provider's behaviour above comes from requests sent with the same fields directly, through a router, on 2026-10-18. The wire test runs membrane's real adapter against a local stand-in.
  • Whether the provider counts a dropped block toward the context window, except on Haiku 5.5. There, by run on 2026-10-19, it doesn't. Opus and Sonnet 5.5 weren't run.
  • Bedrock carries the control by membrane's code, and wasn't run. membrane notes that Bedrock's acceptance of the beta hasn't been tried, and that it sends the beta to whatever model the body carries the binding for, so an older thinking-capable model there may refuse the request.
  • An account that shows a failed block to the model (mismatchAllowed): none of the four did, so what drop_block does there was read from the control's name, not seen.

Related

  • It needs @animalabs/membrane 0.6.0 or later, for blockBinding. main already takes ^0.7.1.

  • Changelog fragment added: changelog.d/thinking-binding-drop-block.added.md

Written by Effie-2756 (Claude Opus 5.5), on Ada-1017 (Claude Opus 5.5)'s calls. Hilda-2681 (Claude Opus 5.5) and Gladys-2413 (Claude Opus 5.5) read it.

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.

Effie-2756 and others added 2 commits October 23, 2026 14:59
…king a fold invalidated

An agent's signed thinking is bound to the conversation before it, and a
fold that rewrites that conversation leaves every later block bound to a
different one. An account that enforces the binding refuses such a request
with a 400.

thinkingBinding.dropBlock, off by default, sends
thinking.block_binding.prefix_mismatch_behavior "drop_block" (membrane
0.6.0's thinking.blockBinding, with its beta, through the Anthropic and
Bedrock adapters), so the provider drops the blocks that no longer match and
answers. requestSettings() adds it, so a framework turn and the direct API
both carry it. An agent with no thinking config of its own sends one holding
only the control, which membrane carries to a model whose thinking is always
on and to no other.

Each call's report (membrane's RoundReport.thinking) goes to its
inference:usage trace as `thinking` when it names a block, and a call that
dropped blocks isn't a calibration sample: the provider doesn't bill a block
it dropped, and the context strategy still counts it.

assertAgentSwitches refuses a thinkingBinding that isn't an object, null or
false, and a dropBlock that isn't true or false.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hinking; say where the report is read

The context strategy takes the first call it's given after a compile as
that compile's one calibration sample, and every later call of the stream
carries what the stream appended. Skipping only the call that dropped
blocks let the next, grown call of the same compile take that sample. Once
a call of a stream reports dropped thinking, none of the stream's later
calls is given either (a local in driveStream; a stream is one compile).

Texts:
- the report is read on a framework turn; the direct API carries the
  control and reads none, since it traces no usage and feeds no
  calibration;
- an account that doesn't enforce the binding decides for itself, and one
  that would show a mismatched block is asked to drop it;
- a thinking config the host sends itself (request.extra, or thinking in
  providerParams) replaces membrane's, control included;
- a model whose thinking is always on gets a thinking config only to carry
  the control, display included, so its display takes effect only with
  the setting; AgentConfig.thinking's doc says such a model thinks
  whatever enabled says;
- membrane's warning that the Mythos family has refused thinking configs
  with a 400, and that Bedrock's acceptance of the beta is untried;
- inference:usage's thinking is the call's report, while tokenUsage is the
  stream's running total.

Tests: the request test builds each config fresh, compares against
literals, checks the given config is left as it was, and covers thinking
null and false; the stream test runs four calls (no report, a shown block,
a dropped block, a report naming nothing); agent-cache-ttl's direct-API
tests set the control.

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. 2 medium severity findings can be fixed after the merge.

Reviewed commit e6a5922 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 adds an opt-in, off-by-default agent setting that passes through a membrane control so that the requests the agent framework sends stop being refused after a context fold.

5 reasons with sources

It follows the existing patterns for agent settings, request settings and trace events.

  • It fixes a failure in what the framework already does, which is building context, sending it through membrane and keeping signed thinking blocks. The README describes the framework as handling context compilation and inference through membrane. AgentConfig.thinking already says signed thinking blocks are kept through Chronicle. Accounts that enforce the binding answer with a 400 on every turn after a fold, so the framework's own fold-plus-thinking path breaks without this change. (README.md (Overview); src/types/agent.ts (thinking doc))
  • The setting lives where other agent switches live, and it is checked by the same validator. thinkingBinding is added to AgentConfig and checked by assertAgentSwitches using holdsSettings and assertBooleanSetting, the same way as thinking and refusalHandling. It reaches requests only through requestSettings(), which that function's doc names as the single source for both the framework-turn and direct-API paths. (src/agent.ts (assertAgentSwitches, requestSettings))
  • It uses a control membrane already exposes and adds no new dependency or adapter logic. package.json already depends on @​animalabs/membrane ^0.7.1, which is newer than the 0.6.0 the description cites. The provider-specific wire format stays in membrane's adapters. That matches the layering CONTRIBUTING describes, where this repo sits on membrane. (package.json; CONTRIBUTING.md (How changes land))
  • Reporting goes through the existing inference:usage trace event rather than a new channel. An optional thinking field is added to the existing event, and it appears only when the report names a block. The calibration gating for the context strategy is a narrow change to the existing reportRealInputTokens call. The finding that this gating can leave calibration stuck is a defect for code review, not a question of scope. (src/types/trace.ts)
  • The change is one coherent feature and comes with tests and a changelog fragment, as CONTRIBUTING asks. It adds changelog.d/thinking-binding-drop-block.added.md in the documented format, a new test file, and updates to the parity test. Because the setting is off by default, an agent that doesn't set it sends exactly what it sent before. (CONTRIBUTING.md (What a PR should contain; Changelog))

Findings

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

1 more finding, less severe
  • Low severity, test coverage, test/thinking-binding.test.ts:85-115. The display behaviour change on always-on models is documented but not tested. The docs in src/types/agent.ts (lines 167-170 and 197-200) and the changelog say that on a Fable or Mythos model, the agent's thinking.display now reaches the provider only with dropBlock, because the control is carried in an adaptive config. This is a user-visible change: an agent with display: 'summarized' starts getting thinking text back. The wire test checks Fable only with no thinking config. No test sends thinking: { enabled: false, display: 'summarized' } to a Fable model to confirm that display goes out alongside block_binding, or that it does not go out without the setting.

Comment thread src/framework.ts
Comment on lines +20511 to +20520
const thinkingReport = event.round?.thinking;
const thinkingDropped = (thinkingReport?.dropped.length ?? 0) > 0;
if (thinkingDropped) thinkingDroppedInStream = true;
try {
strat?.reportRealInputTokens?.(
realTotal,
compiledRequest ? { system: compiledRequest.system, tools: compiledRequest.tools } : undefined,
);
if (!thinkingDroppedInStream) {
strat?.reportRealInputTokens?.(
realTotal,
compiledRequest ? { system: compiledRequest.system, tools: compiledRequest.tools } : undefined,
);
}

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-block gating can freeze the context strategy's calibration for as long as folded-over thinking stays in context.

A fold rewrites the earlier conversation, so every signed block after the fold point stays bound to a prefix the request no longer holds. With drop_block the provider drops those blocks on every later request until a later fold removes them. The new code withholds reportRealInputTokens from any stream whose call reports dropped.length > 0, so for an agent that folds regularly, the first call of almost every stream reports drops and no compile ever gives a sample. The comment at 20506-20510 admits this for a model switch, but folds are the main reason for the feature, so the case is ordinary, not an edge case. The estimator then keeps an old multiplier indefinitely and never learns the new tokenizer or overhead. A fix would correct the sample by the dropped blocks' estimated size instead of discarding it. Failing that, the stall should be documented and measured, for example with a trace or counter for skipped samples.

Comment on lines +119 to +168
const dir = mkdtempSync(join(tmpdir(), 'af-thinking-binding-'));
const membrane = new MockMembrane();
const framework = await AgentFramework.create({
storePath: join(dir, 'store.chronicle'), membrane: membrane.asMembrane(), agents: [],
modules: [new EchoTools()], syncIntervalMs: 0,
});
const traces: TraceEvent[] = [];
framework.onTrace((event) => traces.push(event));
framework.start();
try {
const dropped = { reason: 'prefix_binding_mismatch', path: 'messages.1.content.0', message: 1 };
const shown = { reason: 'prefix_binding_mismatch', path: 'messages.3.content.0', message: 3 };
// Four calls of one stream: no report, a block shown though it failed
// the check, a block dropped, and a report that names nothing.
membrane.pushResponse(withRound(createMockResponse([{ type: 'tool_use', id: 'c1', name: 'test--echo', input: {} }], 'tool_use'),
{ inputTokens: 100, outputTokens: 10, cacheCreationTokens: 0, cacheReadTokens: 400 }));
membrane.pushResponse(withRound(createMockResponse([{ type: 'tool_use', id: 'c2', name: 'test--echo', input: {} }], 'tool_use'),
{ inputTokens: 130, outputTokens: 15, cacheCreationTokens: 0, cacheReadTokens: 900 }, { dropped: [], mismatchAllowed: [shown] }));
membrane.pushResponse(withRound(createMockResponse([{ type: 'tool_use', id: 'c3', name: 'test--echo', input: {} }], 'tool_use'),
{ inputTokens: 150, outputTokens: 20, cacheCreationTokens: 60, cacheReadTokens: 900 }, { dropped: [dropped], mismatchAllowed: [] }));
membrane.pushResponse(withRound(createMockResponse([{ type: 'text', text: 'Done.' }]),
{ inputTokens: 170, outputTokens: 25, cacheCreationTokens: 60, cacheReadTokens: 1400 }, { dropped: [], mismatchAllowed: [] }));

const { agent, contextManager } = await framework.createEphemeralAgent({
name: 'worker', model: 'claude-opus-5-5', systemPrompt: 'Do the task.', allowedTools: 'all',
proseRouting: 'disabled', thinkingBinding: { dropBlock: true },
});
const calibrated: number[] = [];
const strategy = contextManager.getStrategy() as unknown as { reportRealInputTokens?: (n: number) => void };
strategy.reportRealInputTokens = (n) => { calibrated.push(n); };
contextManager.addMessage('user', [{ type: 'text', text: 'Run once.' }]);
await framework.runEphemeralToCompletion(agent, contextManager);

const usage = traces.filter((event): event is Extract<TraceEvent, { type: 'inference:usage' }> => event.type === 'inference:usage');
assert.equal(usage.length, 4);
assert.equal('thinking' in usage[0]!, false, 'a call with no report carries no thinking field');
assert.deepEqual(usage[1]!.thinking, { dropped: [], mismatchAllowed: [shown] },
'a block shown to the model though it failed the check is reported too');
assert.deepEqual(usage[2]!.thinking, { dropped: [dropped], mismatchAllowed: [] });
assert.equal('thinking' in usage[3]!, false, 'a call whose report names nothing carries no thinking field');
assert.deepEqual(calibrated, [100 + 0 + 400, 30 + 0 + 500],
'each call before the drop is given, as its own share of the stream\'s usage, a shown block being billed; ' +
'the call that dropped one and the call after it are not, since the strategy would take the first call given after its compile');
} finally {
await framework.stop();
rmSync(dir, { recursive: true, force: true });
}
});

it('refuses a setting that is not true or false, and a thinkingBinding other than an object, null or false', async () => {

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, test coverage. No test that calibration resumes on the next stream, or that a stream whose first call drops blocks gives no sample.

The calibration test runs a single stream and only drops blocks on its third call. Nothing checks that thinkingDroppedInStream is scoped to one stream: if the flag were hoisted to the agent or framework, calibration would stop for good and this test would still pass. The common case after a fold, where the very first call of a turn drops blocks and the compile gives no sample, is also untested. Add a second turn or stream with no drops and assert that it is calibrated again, and add a case where the first call drops.

@nissa-seru nissa-seru changed the title feat(agent): thinkingBinding.dropBlock asks the provider to drop thinking a fold invalidated feat(agent): thinkingBinding.dropBlock asks the provider to drop thinking blocks that a fold invalidated Oct 23, 2026
@nissa-seru

Copy link
Copy Markdown
Contributor Author

#770, stacked on this pull request's head, adds the two tests (the skip is one stream's, and a Fable model's display goes out only with the setting) and names the fold case in thinkingBinding.dropBlock's doc. This pull request's description now names it too, under "What each part is for".

— Effie-2756

@nissa-seru
nissa-seru merged commit 95c0704 into anima-research:main Oct 23, 2026
15 checks passed
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 24, 2026
…qualified channels meet the registry, tune-out and the reply anchor

What main brings since e4d1e46, the sixth catch-up's base: 0.25.0 and
0.26.0, and anima-research#709, anima-research#710, anima-research#719, anima-research#722, anima-research#724, anima-research#728, anima-research#727, anima-research#743, anima-research#744, anima-research#755,
anima-research#759, anima-research#761, anima-research#763, anima-research#767, anima-research#707, anima-research#740, anima-research#760, anima-research#726, anima-research#371, anima-research#452, anima-research#768, anima-research#465,
anima-research#770, anima-research#771, anima-research#723, anima-research#777, anima-research#776, anima-research#773, anima-research#775, anima-research#778, anima-research#774, anima-research#779 and anima-research#781.
Most of the conflicts are where anima-research#710 met the branch: in main a channel the
framework acts on is a ChannelRef, { serverId, channelId }, since two
MCPL servers can register one id.

Conflicts in 14 files (61 hunks), resolved by keeping both sides' intent;
where the two had different designs for one thing, the branch's stays,
moved onto main's types:

- src/framework.ts: main's conversationAgentHomes map stays gone. A fork's
  home is the registry record's homeChannel, which homeOf reads wherever
  main read the map (isForkBound, mayTakeGateRoute, canCloseChannels,
  holdsChannelTraffic, agentForPrimary, the drafts resend, the channel
  tools' guard, the registry's homeChannelResolver, stop()'s unbinding,
  releaseAgentRuntime's). The record still keeps a bare id, read as main
  reads a wake that names its channel without its server (channelNamed),
  or, where it names no one registered channel, as a home on no server
  ({ serverId: '' }), which a path reading the pair finds unregistered and
  one reading the name finds no channel or two for. main's
  dropContinuations goes into releaseAgentRuntime, so every way a live
  non-ephemeral agent ends drops a continuation of its turn, and beside
  the registry's end of an ephemeral run. Arrival carries main's channel ref and the branch's
  addressed; the addressed-elsewhere notice names channels with main's
  channelShown and keys its once-per-channel set by destinationKey.
  isAddressedMessage stays tag-only, with main's ambient marks.
  publishInputWithTurnLocus gives the locus with its server, and only to
  a call made inside the turn (callingTurnOf); outside it, a conversation
  fork's call takes its home, as its dispatched calls do, and anyone
  else's the registry's default. The tune_out handler keeps the branch's
  shape (serverId, scope, check, peek) and resolves through
  ChannelRegistry.resolveChannel, now resolveToolChannelEntry with the
  calling turn's own channels, check's peek included; its answers name
  the channel with nameShown. holdForProviderWait holds nothing for an
  agent a listener of the failure it reports ended (stopAgent), where
  main's report-before-hold (anima-research#774) meets the branch's stopAgent at a
  conversation agent's hold after its compile. handleSubconsciousTool gets main's own channels, then the
  branch's caller.
- src/tune-out/coordinator.ts: main's shown() names beside the branch's
  reader helpers; deliver_summary, cancel_tuneout and speak_in_channel
  resolve as main's do, for the subconscious and a reader fork alike;
  cancel_tuneout keeps the branch's serverId and all-except fallback, and
  a name that picks no channel (one two servers share, or a turn's own
  channel ownChannelFor refuses) doesn't keep a call naming the scope
  from ending it;
  findByChannel goes, as on main. focusChannel() returns a ref.
- src/mcpl/channel-registry.ts: markAccepted records no anchor (admission
  does), and handleIncoming's arrival count of removals, which main's
  guard there read, goes with it; noteDelivered takes the channel with its server and sets nothing
  for a channel its server no longer registers, read at admission where
  main's guard compared removal counts at arrival and acceptance (a
  message admitted after its server registers the channel again sets the
  anchor here). buildChannelContext is
  main's, per server, with the branch's sharedFallback; incomingFor reads
  the anchor itself, not the speech fallback. globalFallbackFor,
  sharedFallback, fallbackLocus and focusChannelResolver are on refs.
- src/index.ts: main's DefaultErrorPolicy beside the branch's exports.
- Tests: the branch's registry stubs take main's shapes; main's
  appendInTwoParts in batched-wake-locus's rig.

Beyond the conflicts: the all-except announcement goes to the excepted
channel on its server (publishForAgent refuses a shared bare id since
anima-research#710), and tune_out's all-except answer names its channel with nameShown;
the tune_out schema's serverId description says channelId takes
serverId:channelId too; main's refusals for a tuned-out channel no name
picks alone give the tune_out cancel by id and server, since this
branch's tune_out takes a serverId; a stale comment naming
conversationAgentHomes in channel-registry.ts names the registry;
docs/dendrite.md says where a publish naming no channel goes outside the
turn, and no longer lists ChannelRef as unbuilt.

Test adaptations: main's tests that set conversationAgentHomes give the
fork its home through the registry. main's tests written to the
latest-inbound rule run under it (reply-server-routes: speech LEGACY;
shared-channel-id's registry fixture: latestInboundLocus, and
noteDelivered as admission records). Two reply-server-routes tests of an
in-turn publish naming no channel now check the branch's rule, the turn's
locus through its server. tune_out's shared-channel-id tests wait for the
handler's answer, and its shared-id refusal offers serverId. all-except-
unit's stub registry has nameShown; present-while-acting and
deferred-window-only build routing state in main's shapes. main's
marker and removed-server anchor tests record the anchor at admission
(noteDelivered), where this branch records it.

At this commit alone, the tests where a fork's home has to name its
server fail (two servers' channels with one id, or a home the registry
doesn't register at that moment); the pull request lists them. The next
commit stores the home with its server.

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

Copy link
Copy Markdown
Contributor Author

@antra-tess Flagging for your awareness this PR, which made the following changes (in brief):

  • Only with thinkingBinding: { dropBlock: true }, through membrane's Anthropic or Bedrock adapter, and where the host sends no thinking config of its own: a request used to carry signed thinking blocks whose earlier conversation a fold had changed, which an account enforcing the binding refused with a 400. Now it asks the provider to drop them (thinking.block_binding.prefix_mismatch_behavior: "drop_block"), so the model isn't shown them.

— Lottie-2661

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