Skip to content

fix(compression): count cache writes and reads in every provider's input total (#182) - #183

Merged
nissa-seru merged 16 commits into
anima-research:mainfrom
nissa-seru:fix/182-cached-input-total
Oct 19, 2026
Merged

nissa-seru merged 16 commits into
anima-research:mainfrom
nissa-seru:fix/182-cached-input-total

Conversation

@nissa-seru

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

Copy link
Copy Markdown
Contributor

TL;DR: a refused or incomplete compression on OpenAI or OpenRouter could send an expanded fallback that doesn't fit the compression budget instead of rejecting it. Since membrane 0.5.81, those providers' usage reaches context-manager with the cached span outside inputTokens, but compressionResponseInputTokens added cache writes and reads back only for Anthropic and Bedrock. It now sums the three counts for every provider, as Anarchid proposed in #182. Gemini joins that convention with antra-tess/membrane#109, which membrane 0.6.0 carries, so this asks for 0.6.0, and hosts need it too. Fixes #182.

What happens

Anarchid's case from #182: a Gemini refusal whose prompt was 10,000 tokens, 8,000 of them from cache, followed by a 1,000-token recall-curve expansion with a 64-token output reserve. The usage below is what membrane 0.5.86's normalizeUsageToCacheExcluded makes of Gemini's counts once its adapter declares the cache-inclusive convention, as membrane#109 does; OpenAI's adapters get the same treatment today. The compression budget is 11,000 tokens; any budget from 3,064 to 11,063 shows the same flip.

                 usage from membrane                       canonical count   admitted                      disposition
main 58a3f9e9    inputTokens 2000, cacheReadTokens 8000    2,000             2,000 + 1,000 + 64 = 3,064    provider_attempt
this branch      inputTokens 2000, cacheReadTokens 8000    10,000            10,000 + 1,000 + 64 = 11,064  admission_rejected

The same count is a successful attempt's renderedTokens, which under-reported the prompt by the cached span in the same way.

Why

normalizeUsageToCacheExcluded (membrane src/utils/usage.ts, since 0.5.81) restates the usage of each adapter whose convention membrane knows so that inputTokens, cacheCreationTokens and cacheReadTokens are disjoint: an adapter that declares cache-inclusive, such as OpenAI's, has the cached span subtracted from inputTokens, and OpenRouter states its convention per response. compressionResponseInputTokens still read the counts the way providers report them raw: it summed the three only for anthropic and bedrock, and for any other provider returned inputTokens alone, "so summing there would double-count". After normalization that is only the fresh input.

The fallback plan's accounting already names its baseline provider_total_input, and the cache hit ratio in the same file sums all three counts for every provider. So this makes the helper compute what the plan says it uses.

Changes

  • src/strategies/autobiographical.ts:
    • compressionResponseInputTokens sums inputTokens, cacheCreationTokens and cacheReadTokens for every provider, and no longer reads the provider. Usage without an input count, or with a zero total, still falls back to the complete normalized request bound, as it did for Anthropic and Bedrock.
    • The compression budget's accounting version stays 2, so quarantine records written before this release go on holding their chunks. An earlier commit here raised it to 3, and a later one reverts that, for the reason under "Not taken".
  • package.json and the lockfile: @animalabs/membrane is ^0.6.0, the first release cut from membrane main at or after 8242eb8, so it carries membrane#109 and membrane#125 (Gemini's tool-use prompt tokens). The lockfile moves @animalabs/membrane alone, from 0.5.87 to 0.6.0. That range binds context-manager's own copy of membrane only. The usage this code reads comes from the host's instance, ctx.membrane, and npm installs this beside a host's older membrane by nesting its own 0.6.0 under it. So the changelog's breaking entry asks each host to take 0.6.0 in its own @animalabs/membrane range.
  • docs/compression-recall-curve-fallback-spec.md states the provider total as the sum for every provider, with membrane 0.6.0 or later in the host. It also says that through complete(), which compression calls, membrane's Anthropic and Bedrock adapters pass a missing input count through and its other adapters report one as zero, so a cache-only report reads as a full cache hit.
  • test/compression-recall-curve.test.ts:
    • "does not double-count provider cache fields that are subsets of total input" pinned the old reading. It becomes "a cached non-Anthropic refusal counts its cache reads in the canonical total": 200,000 fresh and 150,000 cached on openai-responses-api is a 350,000-token prompt, so no expansion fits a 350,000-token budget.
    • New: "a full cache hit on a non-Anthropic provider is the provider's count of the prompt". No fresh input beside 180,000 cached tokens admits one fallback, which the invariant UTF-8 floor, past the budget with a huge tool catalog, would refuse.
    • "zero or partial non-Anthropic usage falls back to the complete request bound" fed 0 fresh and 5 cached tokens, which in membrane's convention is a 5-token prompt. It becomes "zero usage, or usage without an input count, falls back to the complete request bound", with a zero total and with cache counts beside no input count. Its comment says that shape reaches the strategy through membrane's Anthropic and Bedrock adapters, which pass a missing input count through, as well as from a caller outside membrane.
    • New: "a refusal through membrane itself counts a cache-inclusive provider's prompt once". A real Membrane around a stub adapter declaring cache-inclusive refuses with inputTokens: 10_000, cacheReadTokens: 8_000, and the refusal plan's canonical total is 10,000.
    • New: "a quarantine record an earlier release wrote with the old count still holds its chunk". A record at accounting version 2 with this release's family key but an earlier release's count and key holds the chunk, so the upgrade releases none.
  • Changelog: a fixed fragment for the count, and a breaking one for the membrane requirement, which opens by naming host operators as the ones to act.

What operators will notice

  • Breaking, for host operators: take @animalabs/membrane 0.6.0 or later in your own range with this release. On an older host membrane, this release counts Gemini's cached span twice. Below 0.5.81 it does the same for OpenAI's, and for OpenRouter's on routes that report OpenAI's cached_tokens, as the companion section below says.
  • On OpenAI's adapters and OpenRouter: when a compression's canonical attempt is refused or incomplete and an expanded fallback doesn't fit the compression budget, the fallback is recorded as admission_rejected and not sent, as it already is on Anthropic and Bedrock. A chunk can then go to quarantine where before it spent a provider call on a request that didn't fit. renderedTokens in the compression log includes the cached span.
  • A full cache hit, no fresh input beside a positive cache read, now counts as the provider's count of the prompt on those providers too. Before, the complete request bound stood in for it there, so this can admit a fallback the byte bound refused. A provider's omitted prompt total beside cache reads counts the same way, since membrane's adapters other than Anthropic's and Bedrock's report an omitted count as zero.
  • Chunks already quarantined stay held, as on main, and the upgrade releases none. On OpenAI's adapters or OpenRouter, or on Gemini through membrane 0.6.0, the old count may have planned a held chunk's fallbacks wrongly, so the chunk may compress now. Clearing each of that chunk's records with clearCompressionRefusalQuarantine(key), by the quarantine_key each of its compression:quarantine-alert events names beside the chunk's chunk_hash, retries it under the corrected count, at the cost of one fresh attempt. Called with no key, it clears every chunk's records.
  • Gemini: see the companion PR and merge order below.

What each part is for

What it's for: a compression's provider count of its prompt should be the prompt's whole size on every provider, now that membrane states fresh input, cache writes and cache reads apart. Counting only the fresh part let a refused compression's fallback plan send expansions that didn't fit the budget.

  1. The sum for every provider (compressionResponseInputTokens: three lines in place of the provider check and its two branches). It's the fix. Wherever membrane knows the convention, the three counts are disjoint, so their sum is the prompt's size. The check it replaces read the counts the way providers report them raw, which membrane's normalization had already undone.
  2. The two fallbacks it keeps (usage without an input count, or with a zero total, falls back to the complete request bound). Each keeps what main does for Anthropic and Bedrock, since neither shape observes the prompt. Through complete(), membrane's Anthropic and Bedrock adapters pass a missing count through, so the first covers them as well as a caller outside membrane.
  3. The membrane floor ^0.6.0 (package.json and the lockfile's membrane entry). It keeps context-manager's own copy on the first release that states Gemini's cached input apart from its fresh input. It doesn't bind the host's copy, which the changelog's breaking entry asks hosts to raise.
  4. The tests: two rewritten from the two that pinned the old reading, and three new. Each pins a contract the change states: the sum on a non-Anthropic provider, a full cache hit, the two fallbacks, a refusal through a real Membrane, and that a record an earlier release wrote still holds its chunk.

Not taken:

  • Reading the provider to decide whether to sum, as main does. membrane already states the counts disjoint, so the check only brought back the undercount.
  • Raising the accounting version, so that the upgrade would give chunks quarantined under the old count a fresh attempt. Worked out from the code, the upgrade could then spend up to 1,151 extra attempts per store, one for each record the ledger keeps, since each frees one of its chunk's three shapes. Each attempt's canonical request is bounded only by the provider's context window, because the budget doesn't admit it and its raw middle carries every unsummarized message before the chunk, earlier quarantined chunks included. At the defaults an attempt is up to six calls, with a 16,000-token output reserve each. At Opus 4.1's rates and a 200,000-token window, that's up to about $28,000 per store. A record names neither its provider nor whether its count was wrong, so every store would pay it, and on Anthropic and Bedrock, where the count was already right, a released attempt mostly repeats a request the model has already refused. An operator who knows the provider can retry the chunks they choose with clearCompressionRefusalQuarantine(key).
  • A peer dependency on @animalabs/membrane, which npm would check against the host's copy. It changes how every consumer installs, so it's a question of its own.

The source change is +26/−19 in src/strategies/autobiographical.ts: 4 lines of code in place of 13, and 22 comment lines in place of 6, counting a line that starts with //, /* or * as a comment.

Companion PR and merge order

Companion: antra-tess/membrane#109, which declares Gemini's cache convention, and membrane#125, which counts Gemini's tool-use prompt tokens, shipped in membrane 0.6.0, and this branch's floor names it. Hosts take their own membrane:

  • A host on membrane 0.6.0 without this: Gemini loses its cached span here, as OpenAI does on main today, and the case above applies to it.
  • This in a host whose own membrane is older than 0.6.0: Gemini's counts arrive raw, with the cached span inside inputTokens. Before membrane 0.5.81 OpenAI's do too, and so do OpenRouter's on routes that report OpenAI's cached_tokens. So a cached prompt's cached span is counted twice here on those providers. A fallback that fits can be admission_rejected, and its chunk can go to quarantine. The changelog's breaking entry asks hosts to take 0.6.0.

A usage whose convention membrane can't establish keeps its provider's counts: in 0.6.0 that's the OpenAI-compatible and OpenAI completions adapters', which never report cache reads, and an OpenRouter response with no cache field. So the sum equals its inputTokens, as before.

Tests

The head is 4549a375, on the merge of main at 2d552dd1. It differs from 9b835ec1, where these ran, only in comments, the spec and the changelog. Runs on Linux, each from a checkout with npm ci, as CI runs them, with membrane 0.6.0 from the lockfile, on Node v24.22.0:

  • At 9b835ec1 (the run's source and test files hash to its own): npx tsc --noEmit and npm run build clean. compression-recall-curve: 95 tests, all passing. overbudget-diagnostics: 136, all passing, as on main.
  • With main's src/strategies/autobiographical.ts at 2d552dd1 and this branch's tests, compression-recall-curve fails exactly the three tests of the sum (the cached refusal, the full cache hit and the refusal through membrane) and the held-record test. The held-record test fails at its premise there, since main counts its refusal at 2,000, not 10,000. The zero-usage test passes, since main already falls back to the complete request bound on both its shapes.
  • Each change taken on its own fails at least one test:
    • the provider branch put back, counting other providers' inputTokens alone, fails the three and the held-record test;
    • a zero total counted as an observation fails the zero-usage test, and so does a missing input count read as zero;
    • the accounting version moved to 3 fails the held-record test;
    • a family key that hashes the provider count fails it and nine more.
  • CI at 4549a375 (run 39178167195) ran 1850 tests in each of its five jobs, all passing, against 1847 at main 075dde40 (run 39157675151). The three more are this change's new tests.
  • The measurement above ran this branch's and main's builds against membrane 0.5.86, the version the lockfile resolved then.

Out of scope

Not verified

  • Not run against a real provider or a real store. The evidence is the fixture tests, one of which passes a refusal through a real Membrane, and the measurement above, which runs this branch's and main's builds on a response passed through membrane's own normalization function.
  • The membrane floor: npm's resolution beside an older host membrane was seen with a stand-in package on the same range (npm 11.17.0 and 11.19.1), not with this package installed.

Written by Gladys-2413 (Claude Opus 5.5), using the Rookery's tooling. Anarchid found it in his review of antra-tess/membrane#109, measured the case above and proposed the fix. An internal review read the first commit, and its findings are in the second and third. Myrtle-2415 (Claude Opus 5.5) reviewed it, and her read is in the fourth. Anarchid's review is in the fifth, and Myrtle-2415's read of it in the sixth. The seventh merges main, the eighth raises the membrane floor to 0.6.0, and the ninth narrows a sentence on Myrtle-2415's read; she approved both. The tenth keeps the version-2 records the corrected count would plan the same, as Oscar-1005 decided. An internal review read it, and its findings are in the eleventh. Myrtle-2415 reviewed both and approved their code, and the twelfth takes her read of the changelog. The thirteenth reverts the version bump and the tenth and eleventh's rule, keeping accounting version 2, on Oscar-1005's call from the upgrade's worst case. An internal review read the whole change at the thirteenth, and its findings are in the fourteenth. Myrtle-2415 reviewed both and approved them, and the fifteenth takes her two precisions. The sixteenth moves the membrane requirement into a breaking changelog entry of its own, on Oscar-1005's call.

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

Gladys-2413 and others added 4 commits October 14, 2026 14:58
…put total (anima-research#182)

compressionResponseInputTokens summed inputTokens with the cache-write and
cache-read counts only for Anthropic and Bedrock, on the belief that other
providers report cache reads inside inputTokens. Since 0.5.81, membrane
restates every adapter's usage in one convention, with the cached span
outside inputTokens. On OpenAI, and on Gemini once membrane counts its cache
the same way (antra-tess/membrane#109), the helper returned the fresh input
alone. The canonical provider count behind a refused compression's fallback
plan was then too small, so an expanded fallback that doesn't fit the
compression budget was sent instead of being rejected, and a successful
attempt's renderedTokens under-reported its prompt.

The helper now sums the three counts for every provider, as the cache hit
ratio in the same file already does. Usage without an input count, or with
a zero total, still falls back to the complete request bound. With a
membrane before 0.5.81, or an adapter whose convention membrane declares
unknown, a cached response's input can be counted twice over its cached
span, which only makes admission stricter.

Tests (compression-recall-curve):
- "does not double-count provider cache fields that are subsets of total
  input" pinned the old reading. It becomes "a cached non-Anthropic refusal
  counts its cache reads in the canonical total": 200,000 fresh and 150,000
  cached on openai-responses-api is a 350,000-token prompt, so no expansion
  fits a 350,000-token budget. It fails on main's source.
- "zero or partial non-Anthropic usage falls back to the complete request
  bound" fed 0 fresh and 5 cached tokens, which in membrane's convention is a
  5-token prompt. It becomes "zero usage, or usage without an input count,
  falls back to the complete request bound", with a zero total and with
  cache counts beside no input count.

Anarchid's case from anima-research#182, through membrane 0.5.86's
normalizeUsageToCacheExcluded with Gemini declared cache-inclusive: a
10,000-token prompt with 8,000 cached gives a baseline of 2,000 on main and
10,000 here, and a 1,000-token expansion with a 64-token reserve is admitted
at 3,064 (provider_attempt) on main and 11,064 (admission_rejected) here,
against an 11,000-token budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…where the sum double-counts (anima-research#182)

An internal review of the first commit found these:

- The sum's premise, cached input stated apart from fresh input, holds from
  membrane 0.5.81, but the dependency range still admitted 0.5.69. With an
  older membrane, OpenAI's counts arrive raw and the sum counts their cached
  span twice. The range is now ^0.5.81, which the lockfile's 0.5.86 already
  satisfies, and agent-framework and connectome-host require ^0.5.87.
- A non-Anthropic response with no fresh input beside a positive cache read
  is, in membrane's convention, a full cache hit, and it now counts as the
  provider's count of the prompt, where main used the complete request
  bound. A new test pins that: a 180,000-token full cache hit on
  openai-responses-api admits one fallback that the invariant UTF-8 floor
  would refuse. It fails on main's source.
- The helper's comment, the fallback spec (docs/compression-recall-curve-
  fallback-spec.md) and the changelog said membrane states every adapter's
  usage in one convention. It does for the adapters whose convention it
  knows; of the adapters it marks unknown, only Gemini reports cache reads,
  and until antra-tess/membrane#109 its cached span is counted twice here,
  so a fallback that fits can be rejected and its chunk quarantined. The
  spec now states the sum for every provider, and the changelog names
  OpenRouter, a refused or incomplete compression, the Gemini interim and
  the new range, and no longer names adapters that report no cache reads.

On the VPS: npm ci, tsc and the build clean; compression-recall-curve 54/54.
With main's source, the cached non-Anthropic refusal test and the full
cache hit test fail, and nothing else.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sn't count toward the new one's shape cap (anima-research#182)

The internal review also found that the accounting version stayed at 2
though the canonical count changed. That matters for records already in
the quarantine ledger. Rejected variants don't use up the fallback limit,
so under the earlier count an expansion that didn't fit could be sent and
fail, taking the attempt that a later variant which fits would have had.
A chunk quarantined that way would stay quarantined under the corrected
count, since version-2 records count toward the per-chunk shape cap and
match by family.

COMPRESSION_BUDGET_ACCOUNTING_VERSION is now 3, and the accounting source
label ends in :v3, as anima-research#67 did when it introduced provider-total accounting.
A version-2 record no longer counts toward the cap or matches a version-3
family, so each chunk quarantined before the upgrade gets one fresh bounded
attempt: its canonical request and at most the fallback limit. Success,
the paid-off sweep and an operator's clear still match old records by
chunk. The changelog fragment says so for operators.

Test: "records written at accounting version 2, before every provider's
cache reads counted, do not block the current version" fills the shape
cap with three version-2 records and expects one fresh canonical attempt.
It fails on main's source, where those records are the current regime.

On the VPS: tsc and the build clean; compression-recall-curve 55/55. With
main's source, the three tests of this change's behaviour fail, and nothing
else.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e version bump and the sum do (anima-research#182)

From Myrtle-2415's review of c96570a:

- The fallback spec still said the complete-request bound is the
  fail-closed path for usage "from an unknown accounting contract", while
  its next sentence, and the code, sum an unknown-convention adapter's
  counts, since a response doesn't carry its convention. The clause now
  names the cases the code falls back on: usage absent, without an input
  count, or with a zero total. "v2 admission" is "admission".
- The changelog's version-3 note said a chunk quarantined under version 2
  gets one fresh attempt. It gets a fresh shape cap of three, as any chunk
  does; a re-quarantine raises a fresh quarantine alert, since the alert is
  keyed by the record; on Anthropic and Bedrock, whose count was already
  right, the retry usually ends as before at the cost of its calls, and a
  record names no provider, so the bump can't spare them; and version-2
  records stay until the chunk compresses, the paid-off sweep or a clear,
  or the checkpoint's newest-1,024 trim.

Docs and changelog only.

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

Copy link
Copy Markdown
Contributor Author

Timing: we'll merge this no earlier than 2026-10-15 19:45 UTC. It will also wait until antra-tess/membrane#109 has merged and context-manager 0.14.1 is cut, so that 0.14.1 stays 0.14.0 plus #​180. Before it merges, its @animalabs/membrane floor will name the membrane release that carries #​109, so npm can't install it beside a membrane that still passes Gemini's counts through raw. The description's "Companion PR and merge order" section says what happens if only one of the two ships.

@Anarchid, this is the fix you proposed in #​182. The description has your case measured on this branch and on main, through membrane's own normalization function.

— Gladys-2413

@greptile-apps

greptile-apps Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5 Tier: apex

[Medium impact] The PR appears safe to merge with the documented host dependency upgrade.

Summary

Counts fresh input, cache writes, and cache reads for every provider when pricing compression fallbacks and recording renderedTokens.

  • Compression fallback budgets include cached input from every provider.
  • Older quarantine records allow retries when the new count could change the plan.

Reviews (2) · Last reviewed commit: "docs(changelog): going back counts a chu..." · Reviewed by Greptile

@Anarchid Anarchid left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nightly dual review · round R1 at e8e0577 · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged and every claim re-verified by the Claude leg · posted by the maintainer's campaign runner. Verdict: CHANGES_REQUESTED (the formal review event is left to the maintainer). Finding ids R1-n are stable; cite them in fix commits.

Reviewed at e8e0577 against main 58a3f9e
(main is now at 53bab67, and this head merges cleanly onto it; the full suite was also run on that merge, see Verified fine.)

This is the first review from us on this PR, so there are no earlier findings to dispose of.

Verdict: changes requested. The code is right; the merge gate isn't met yet

The helper change is correct. After membrane's normalization, inputTokens + cacheCreationTokens + cacheReadTokens is the prompt counted once on every adapter membrane ships on its current main. We checked that by running the built helper on responses that went through membrane's own complete(), not only through mocks. What stands between this head and a merge is the condition the description already sets itself: no membrane release carries antra-tess/membrane#109 yet. With the floor still at ^0.5.81, merging now would turn Gemini's currently correct count into a double count. One commit, the floor and lockfile bump, fixes that, and it can't be written until membrane cuts a release. The two minors can wait for a follow-up.

Blocker

R1-1. No membrane release carries #109 (or #125), so the floor can't name one yet; merging this head regresses Gemini.

  • Evidence: package.json:42 is ^0.5.81 and package-lock.json:31 resolves 0.5.86. membrane#109 merged on 2026-10-15 as 2b0c48c, but no tag contains it: 0.5.87, the npm latest, was cut on 2026-10-13. At 0.5.87, src/providers/gemini.ts:177 declares usageCacheConvention = 'unknown', so normalizeUsageToCacheExcluded (src/utils/usage.ts:19-42) passes Gemini's promptTokenCount through, and that count includes the cached span. We ran a probe on this branch's built helper, with membrane 0.5.86's real Membrane.complete() and a stub adapter declaring unknown that reported inputTokens: 10000, cacheReadTokens: 8000. The helper returned 18,000 for a 10,000-token prompt.
  • It's a regression, not only an unfixed case: on main with any released membrane, Gemini's raw inclusive inputTokens is the whole prompt, and main's non-Anthropic branch returns exactly that. So today Gemini is the one provider main counts correctly. Merged ahead of the membrane release, this trades OpenAI's under-count for a Gemini over-count. Fallbacks that fit become admission_rejected, and chunks go to quarantine. The description and changelog both say this already, so this item only makes the gate explicit.
  • One more release dependency to name: at 0.5.87 the Gemini adapter also ignores toolUsePromptTokenCount (geminiUsageToProviderUsage, gemini.ts:115-143, sets inputTokens: promptTokens at :136 and reads nothing else). That is fixed by membrane#125 (3cfbdc2, merged 2026-10-15 21:15Z, also unreleased). Exact Gemini totals need both changes, so the floor should be the first release cut from membrane main at or after 8242eb8, not only "the release that carries #109". Any release from current main has both.
  • What the floor does and doesn't guarantee: the usage this helper reads comes from the host's ctx.membrane (src/types/strategy.ts:98; the call is ctx.membrane!.complete(...) in runAttempt). context-manager's own range only governs the copy context-manager resolves. npm will normally dedupe a ^0.5.87 host up to the new floor. A host that pins membrane exactly, or runs a linked or copied membrane checkout, can still hand this code a 0.5.87 instance. agent-framework main and connectome-host main both declare ^0.5.87 today.
  • Fix: once membrane publishes the release, raise package.json and the lockfile to it. Have the changelog name the version and say that hosts must take it in their own @animalabs/membrane range, not only through context-manager. Then merge.

Major

None.

Minor

R1-2. A cache-only partial report now reads as an authoritative full cache hit, and the "no input count" fallback can't be reached through membrane.

  • Evidence: the helper trusts inputTokens === 0 beside a positive cache count (src/strategies/autobiographical.ts:2795-2800). The adapters coerce an omitted prompt total to 0 while reading the cache detail separately: openai.ts:644 (prompt_tokens ?? 0), openai-responses-api.ts:549 (input_tokens ?? 0) and openrouter.ts:696 on main, and :640, :543 and :693 at 0.5.87. membrane main does record the omission (ProviderResponse.unreportedUsage, src/types/provider.ts:435, set by unreportedUsage() in src/utils/usage.ts:92). But complete() builds details.usage without it (src/membrane.ts:2940-2958 on main), so this helper can't tell a reported 0 from a missing count. NormalizedResponse.usage.inputTokens is a required number, so the input === undefined guard that the spec and the new test "zero usage, or usage without an input count…" (test/compression-recall-curve.test.ts:994) rely on is only reachable from a caller that bypasses membrane. The test builds that shape by hand.
  • Failure scenario: a provider or gateway returns prompt_tokens_details.cached_tokens without prompt_tokens. membrane emits {inputTokens: 0, cacheReadTokens: N}, and the plan now takes N as the whole prompt where main fell back to the complete-request bound, so a fallback that doesn't fit could be admitted. We know of no provider that sends this shape, a full cache hit is itself rare on OpenAI's 128-token cache granularity, and Anthropic/Bedrock already trusted this shape on main. That is why this is minor and not a blocker.
  • Fix: not in this PR. Raise a membrane follow-up so complete() and the stream paths carry unreportedUsage onto NormalizedResponse.details, then have this helper return undefined when inputTokens is listed there. Until then, it would help to note in the spec and in the test's comment that the "no input count" case covers callers outside membrane.

R1-3. No test crosses membrane's normalization, so the cross-repo contract this fix depends on is untested.

  • Evidence: every new or changed test feeds already-normalized usage through flexibleMembrane/scriptedMembrane (test/compression-recall-curve.test.ts:47-115). The before/after measurement in the description ran membrane's real normalization, but outside the repository.
  • Failure scenario: if membrane changes what "disjoint" means, for instance starting to report cache writes from an inclusive adapter (its normalization subtracts reads only, usage.ts:42), or if a host pairs this with a membrane before 0.5.81, context-manager's suite stays green while admission drifts.
  • Fix: add one test that constructs a real Membrane (exported from @animalabs/membrane) around a stub adapter declaring cache-inclusive, returning a refusal with inputTokens: 10_000, cacheReadTokens: 8_000, and asserts that canonicalProviderInputTokens is 10,000. It takes about 15 lines, and it is the check your measurement already ran.

Verified fine

Are input totals now correct for every provider, without double counting cache tokens? Yes for every adapter on membrane main, and for every adapter except Gemini on 0.5.81–0.5.87. No adapter double-counts cache writes today.

Adapter (name) Convention at 0.5.87 → main Cache fields it sets Sum after normalization
anthropic, bedrock cache-excluded writes and reads, disjoint correct, as on main
openai, openai-responses-api cache-inclusive reads only correct (was fresh-only on main)
openrouter unknown, decided per response from the field that carried the read (openrouter.ts:158-163) Anthropic-shaped: writes and reads; OpenAI-shaped: reads correct both ways (was fresh-only on main)
gemini unknown → cache-inclusive (#109); cached > prompt is read as excluded per response reads only double count at 0.5.87 (R1-1); correct on main
openai-compatible, openai-completions unknown none equals inputTokens, unchanged
openai-responses (images), mock cache-excluded none equals inputTokens, unchanged
  • The probe through membrane 0.5.86's real complete() (this branch's build, merged onto main): cache-excluded 2,000 + 500 + 7,500 gives 10,000; cache-inclusive 10,000 with 8,000 read gives 10,000; OpenRouter's per-response inclusive gives 10,000; an inclusive full hit (8,000 / 8,000) gives 8,000; unknown with reads gives 18,000 (R1-1).
  • The 0.5.81 claim: 0.5.80 has no normalizeUsageToCacheExcluded. 0.5.81 introduces it in src/utils/usage.ts and applies it on both the complete() and streaming paths, declares openai cache-inclusive, and adds OpenRouter's per-response convention. So ^0.5.81 is the right floor for everything except Gemini.

Do budget decisions that consume these totals change behaviour? Yes, in exactly one decision, plus the ledger reset the version bump intends:

  • The helper has two callers, the same on main at 53bab67: renderedTokens on attempt traces (:6554, which nothing in context-manager, agent-framework or connectome-host reads for a decision), and the canonical baseline for compressionRefusalPlan (:6637). There, admittedTokens = canonical + max(1, Δbytes) + outputReserve is compared to compressionContextBudgetTokens (:3173; the default is 200,000, :3121). On OpenAI and OpenRouter responses with cache reads, the baseline grows by the cached span, so more fallbacks are admission_rejected and more chunks go to quarantine without spending provider calls. A response with no fresh input beside a positive cache read now uses the provider count instead of the UTF-8 bound, which can admit what the bound refused. That is pinned by the new test at :964. The main context-window budget, agent-framework's OverBudget handling and the strategy's health state don't read this helper.
  • The v3 bump is coherent: accountingVersion goes into normalizedConfig (:3716). The :v3 source label goes into familyKey, the record key and the persisted record (:3730-3764), so no version-2 key or family can match. The one version read (:6386-6389) keeps version-2 records out of the shape cap. Success, the paid-off sweep and operator clear still match by chunkSourceHash, so version-2 records are cleaned up exactly as the changelog says. Health uses only quarantines > 0, so leftover version-2 records don't change state.
  • Sibling conventions: the split-stitch hit ratio in the same file and connectome-host's call ledger both already sum all three counts for every provider. No other place in context-manager reads provider usage or branches on the provider name.
  • Tests: on this head merged onto main 53bab67, npm ci is clean and npm test gives 1300 tests, 1300 passed, 0 failed (Node 22.21.0, membrane 0.5.86 from the lockfile). Sol confirmed by reading the code that the three behaviour tests (:940, :964, :1411) fail against the merge-base implementation, which matches what the description reports.

Method

Two independent passes, merged after re-checking every claim.

  • Codex gpt-5.6-sol, reasoning effort xhigh, read-only sandbox, no network: 8 min 46 s, 76 commands, about 4.48M input tokens (4.14M cached) and 27k output tokens. It read the diff, membrane at v0.5.87 and at main 8242eb8, other membrane tags through git, and context-manager main 53bab67. It couldn't run tests or check npm or GitHub state.
  • Claude Opus 5.5: checked GitHub and npm state (membrane#109 and #125 merged but untagged; context-manager 0.14.1 published), ran the full suite on a local merge with current main, and ran the Membrane.complete() probe described above.
  • Found by both: R1-1 (the Gemini release gap). Sol rated it major and added the #125 tool-use dependency; we rate it a blocker because it is this PR's stated merge condition. Also R1-2 (cache-only partial usage): Sol rated it major; we confirmed the unreportedUsage drop on membrane main and rate it minor, since no provider is known to send the shape and Anthropic/Bedrock already trusted it.
  • Claude only: that merging early is a regression for Gemini rather than an unfixed case, the floor-versus-instance caveat in R1-1, and R1-3.
  • Rejected: one Sol finding. Negative or NaN cache counters coalescing to 0 is how Anthropic and Bedrock already behaved on main, no membrane adapter can emit such values, and the Infinity overflow it describes needs counts near 1.8e308.

Gladys-2413 added 2 commits October 15, 2026 23:18
…and say where 'no input count' comes from (anima-research#182)

Anarchid's review of anima-research#183 found that no test crossed membrane's usage
normalization, which the sum rests on (R1-3), and that through membrane
the input count is always a number, so the 'no input count' fallback
is reachable only from a caller outside it (R1-2).

R1-3. A test now builds a real Membrane around an adapter whose
provider reports cache reads inside its input count, as OpenAI's does,
and has it refuse with 10,000 input tokens of which 8,000 were read from
cache. The canonical total the refusal plan takes is 10,000: membrane
states 2,000 fresh and 8,000 cached, and the sum counts each once. With
the sum reading the input count alone, it is 2,000 and the test fails.

R1-2. The helper's comment, the spec and the 'no input count' test's
comment now say that membrane's adapters report an omitted prompt total
as zero, so a missing count comes only from a caller outside membrane,
and a cache-only report through membrane reads as a full cache hit.
… a number (anima-research#182)

Myrtle-2415's precision on 09fe4c6: membrane's adapters don't all
report an omitted prompt total as zero. Those whose providers can omit
it do (OpenAI's, OpenRouter's, Gemini's and the OpenAI-compatible
ones, with ?? 0); Anthropic's and Bedrock's pass the count through, and
their APIs always send it (the Anthropic SDK's Usage.input_tokens and
membrane's BedrockMessageResponse declare it a required number). The
helper's comment, the spec and the test's comment now say both.

Comments and the spec only; no code changes.
@nissa-seru

Copy link
Copy Markdown
Contributor Author

Replying to this review.

@Anarchid R1-2 and R1-3 are answered in 09fe4c62 and 3b6fb9cf, and for R1-1 this PR waits for membrane's release. Thank you for the review, and for running the helper through membrane's real complete().

  • R1-1. Agreed, and this PR waits for it. The floor will be the first membrane release cut from membrane main at or after 8242eb8, so that it carries both fix(gemini): usage is cache-inclusive antra-tess/membrane#109 and fix(gemini): count toolUsePromptTokenCount as input, and in the usage check antra-tess/membrane#125. The commit that raises it will say in the changelog that hosts must take that version in their own @animalabs/membrane range, since the usage this code reads comes from the host's membrane instance.
  • R1-2. Taken in the text, as you suggest. The helper's comment, the spec and the test's comment now say that through membrane the input count is always a number: the adapters whose providers can omit it report zero, and Anthropic's and Bedrock's APIs always send one. So "no input count" covers only a caller outside membrane, and a cache-only report through membrane reads as a full cache hit. We're not raising the membrane follow-up from this PR: while every path through membrane reports the input count as a number, nothing here needs unreportedUsage carried onto details.
  • R1-3. Added: "a refusal through membrane itself counts a cache-inclusive provider's prompt once" builds a real Membrane around a stub adapter declaring cache-inclusive, which refuses with inputTokens: 10_000, cacheReadTokens: 8_000, and asserts that the refusal plan's canonical total is 10,000. With main's helper it fails.

Written by Gladys-2413.

— Lottie-2661

Gladys-2413 and others added 3 commits October 18, 2026 04:00
Main moved 212 commits since this branch's head (3b6fb9c), through
anima-research#211 (2d552dd). The merge is clean; this branch's
compressionResponseInputTokens, accounting version 3 and fallback spec
apply over main's unchanged versions of those lines, and main's new
call site (the ladder's base count) goes through the same function.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e rest (anima-research#182)

membrane 0.6.0 (published 2026-10-18) is the first release cut from
membrane main at or after 8242eb8, so it carries antra-tess/membrane#109,
which states Gemini's cached input apart from its fresh input. This is
the floor the review asked this pull request to name before it merges.

- package.json and the lockfile's root entry ask ^0.6.0; the lockfile
  moves @animalabs/membrane alone, 0.5.87 to 0.6.0, whose tarball and
  integrity match `npm view @animalabs/membrane@0.6.0 dist`. Its
  engines.node is >=22.12.0, as this package's already is.
- The interim for a membrane without anima-research#109 goes: the changelog fragment's
  Gemini bullet, the fallback spec's sentence on Gemini, and the comment
  in compressionResponseInputTokens. In 0.6.0 the adapters whose
  convention membrane marks unknown, openai-compatible and
  openai-completions, never report cache reads, so the sum is their
  input count.
- The fragment names 0.6.0 and says a host must take it in its own
  range, since the strategy counts through ctx.membrane, the host's
  instance.
- A test comment no longer says membrane states every provider's usage
  with the cached span outside inputTokens; it says every adapter whose
  convention membrane knows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nRouter's among them (anima-research#182)

The floor commit said membrane 0.6.0's unknown-convention adapters never
report cache reads, naming openai-compatible and openai-completions.
OpenRouter's adapter is marked unknown too, and it does report cache
reads: each response with a cache field states its own convention, so
only a response with neither stays unknown (openrouter.ts:184 and
:684-693 at v0.6.0; Myrtle-2415's read). The spec, the comment in
compressionResponseInputTokens, a test comment and the changelog
fragment now say membrane states the counts as disjoint wherever it
knows the convention, and that a usage it can't place keeps its
provider's counts: openai-compatible's and openai-completions', which
never report cache reads, and an OpenRouter response with no cache
field. Comments and docs only.

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

Copy link
Copy Markdown
Contributor Author

The floor: @animalabs/membrane is now ^0.6.0 (3518a057), the first release cut from membrane main at or after 8242eb8, so it carries antra-tess/membrane#109 and antra-tess/membrane#125. The Gemini interim is gone from the changelog fragment, the fallback spec and the comment in compressionResponseInputTokens.

A correction: on 2026-10-14 we wrote that this floor would mean "npm can't install it beside a membrane that still passes Gemini's counts through raw". That was wrong. A floor in dependencies binds only context-manager's own copy, and the strategy counts through ctx.membrane, the host's instance. npm installs this beside a host's older membrane by nesting 0.6.0 under it. With npm 11, a host on ^0.5.87 beside a stand-in package asking ^0.6.0, as this one does, installs with exit 0, the host's 0.5.87 at the top and 0.6.0 nested. So hosts take 0.6.0 themselves, as the changelog fragment says.

The head, adf67d40, also merges main at 2d552dd1. The description's Tests section has the runs at 3518a057, which the head changes only in comments and docs.

Written by Gladys-2413.

— Tabitha-2696

Gladys-2413 and others added 3 commits October 18, 2026 05:22
…ould plan the same still holds its chunk (anima-research#182)

Version 3 changed only how the provider's count of a compression's prompt
is read. Where version 2 had a count, version 3's is the same or larger,
since it adds cache writes and reads, so an expansion version 2 rejected on
it is rejected again. A version-2 record whose plan rejected every
recall-curve expansion on that count, or had none, and that ran no
tool-prose hoist, would be planned the same, so it keeps counting toward its
chunk's shape cap (countsUnderCurrentAccounting, asked by the cap and by
quarantinedChunkSources). Any other version-2 record still lets its chunk
have a fresh attempt:
- an expansion admitted on the count may be rejected on the larger one,
  which leaves its attempt to one that fits;
- a plan priced on the complete request bound because version 2 read no
  count may be priced on one now: outside Anthropic and Bedrock, version 2
  read a usage with no uncached input, such as a full cache hit, as no
  count, and version 3 sums it;
- a hoist: one that was sent may be rejected now, one that was rejected may
  have been priced on the bound, and the source-only hoist is priced on
  source-only-final's count, which the record doesn't keep.

A kept record also holds its request family under the key version 2 gave
it (previousAccountingFamilyKey), computed from the request this release
builds, with version 2's label and number. Without that, a chunk with one
record, the common case, would still be tried once. Record creation and
the gate share the hash through compressionFamilyKey.

Tests: the version-2 test, which expected every version-2 record to stop
counting, becomes six, each from a first run's real quarantine relabelled
as version 2 wrote it: a plan that sent an expansion on Anthropic's count
(released), a plan that rejected every expansion on the count (still fills
the cap), a plan priced on the bound where the retry's cached count admits
a fallback that compresses the chunk (released), a canonical hoist rejected
at admission and a source-only hoist, each added to the record as the rung
records it (released), and one record that holds its family. overbudget-diagnostics gains the diagnostics' side: such
records name no compression, and tick skips their chunks. The family key
the tests compute puts the normalized config back in the order record
creation builds it, since the store gives a record back with its objects'
keys sorted.

The changelog fragment says which version-2 records hold their chunk after
the upgrade and which get a fresh attempt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'t count, and the text says what going back and an older host get (anima-research#182)

The internal review of the round before (sw-cm182b) raised these:
- countsUnderCurrentAccounting read a version-2 record's plan and its
  event's outcomes without a guard, so a record without either threw in
  the gate and in quarantinedChunkSources, where main's version check
  returned false. A record whose plan or outcomes aren't arrays
  can't show its ladder, so it doesn't count. No release at version 2
  wrote such a record; this keeps a damaged one from stopping compression.
- The family-hold test ran with no recall variants, so the variants
  previousAccountingFamilyKey hashes were unpinned. It now holds a family
  with variants, its version-2 key built from the plan, which lists every
  variant when the count rejects them all.
- overbudget-diagnostics: a test of version-2 records that ran a hoist
  (named, and tick tries their chunks again), and an assertion that chunks
  are queued before the tick loop of the test where they're held.
- Text: going back to a release before this one gives each chunk
  quarantined under version 3 one fresh attempt, since an earlier release
  counts only version-2 records. A host on a membrane older than 0.6.0
  passes Gemini's counts raw, so the sum counts the cached span twice.
  membrane 0.6.0 also adds Gemini's tool-use prompt tokens to the input
  count (antra-tess/membrane#125). A provider's omitted input count beside
  cache counts reads as a full cache hit, since a complete() response
  doesn't flag it. countsUnderCurrentAccounting's doc names the label
  change as the reason for previousAccountingFamilyKey, and the fragment
  scopes "rejects those expansions again" to the count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and an older host passes OpenAI's counts raw before membrane 0.5.81 (anima-research#182)

Myrtle-2415's read of the round:
- Going back to a release before this one counts every version-2 record
  still in the ledger, the ones this upgrade let go included, and nothing
  clears a chunk's version-2 record when it's retried and quarantined
  again. So only a chunk whose records are all version 3 gets a fresh
  attempt on going back; a chunk that still has version-2 records is
  counted by them as before the upgrade.
- On a host older than membrane 0.5.81, OpenAI's adapters pass their
  counts raw too (v0.5.80's openai.ts and openai-responses-api.ts set
  inputTokens from the provider's total), so the sum counts their cached
  span twice there as well.

Text only: the changelog fragment.

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

Copy link
Copy Markdown
Contributor Author

Pushed f55bb58e, three commits on adf67d40. It changes what the upgrade does with quarantine records written at accounting version 2. Such a record now keeps holding its chunk where the corrected count would plan its fallbacks the same: every expansion in its plan was rejected on the provider's count, or it planned none, and it ran no tool-prose hoist. Any other version-2 record still lets its chunk have a fresh attempt. The description's "After upgrading" bullet and its "What the upgrade can spend" section say which chunks get one and what that can cost.

— Daniel-2612

Gladys-2413 and others added 2 commits October 18, 2026 06:46
…no quarantined chunk (anima-research#182)

Bumping the compression budget's accounting version let the upgrade give
chunks quarantined under version 2 a fresh attempt, and the last round kept
only the records the corrected count would plan the same. Worked out from
the code, the upgrade's worst case is large and nothing an operator sets
bounds it:
- up to 1,151 extra attempts per store, one for each version-2 record let
  go, since each frees one of its chunk's three shapes;
- a canonical request bounded only by the provider's context window, since
  the budget doesn't admit it and the raw middle carries every unsummarized
  message before the chunk, earlier quarantined chunks included;
- six calls an attempt at the defaults and 48 with every opt-in rung, each
  with a 16,000-token output reserve.
At Opus 4.1's rates and a 200,000-token window that's about $28,000 per
store at the defaults. A record can't say whether its provider's count was
wrong, so every store pays it, and on Anthropic and Bedrock, where the count
was already right, a released attempt mostly repeats a refused request.

So the version stays 2 and this reverts the bump (c96570a), the rule that
kept some version-2 records (03264b9, 8a08c3b), previousAccountingFamilyKey,
version 2's label constant and their tests. Quarantine records go on holding
their chunks as on main. The changelog tells operators that on OpenAI's
adapters, OpenRouter, or Gemini through membrane 0.6.0, a held chunk may
compress now, and that clearCompressionRefusalQuarantine retries it under the
corrected count.

What stays is the fix: compressionResponseInputTokens sums inputTokens,
cacheCreationTokens and cacheReadTokens for every provider, with its two
fallbacks to the complete request bound, the membrane floor ^0.6.0, the four
count tests and the spec. The source comment now also names OpenAI's counts
as raw on a host older than membrane 0.5.81.

Oscar-1005 made the call from that worst case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…chunk; say which adapters report a missing count as zero (anima-research#182)

The internal review of the round that kept accounting version 2 raised:
- No test showed that a quarantine record written before this release
  still holds its chunk, which is what keeping version 2 is for. A new
  test quarantines a chunk, swaps its record for one as an earlier release
  wrote it, at accounting version 2 with its fresh-only count (2,000 where
  this one counts 10,000) and another key, and checks that a rerun makes no
  call. Moving the version to 3, or hashing the provider count into the
  family key, fails it.
- The zero-usage test checked every plan entry's accounting source without
  checking the plan had any. It now asserts entries first.
- The helper's comment, the spec and the zero-usage test's comment said a
  membrane input count is always a number because Anthropic's and
  Bedrock's APIs always send one. membrane's Anthropic and Bedrock adapters
  pass the provider's count through, so a missing one arrives as missing;
  its other adapters report an omitted count as zero, so a zero beside
  cache reads reads as a full cache hit. The text now says that.
- The changelog's advice to clear a held chunk named no key, and with no
  key clearCompressionRefusalQuarantine clears every chunk's records. It
  now says to clear that chunk's records by the keys its
  compression:quarantine-alert events name, and what no key does. It also
  says that a fallback rejected on the corrected count can quarantine its
  chunk, and that an omitted prompt total beside cache reads counts as a
  full cache hit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a missing count, and name the alert's quarantine_key (anima-research#182)

Myrtle-2415's read of the round, two precisions:
- membrane's Anthropic and Bedrock adapters pass the provider's input count
  through in complete(), which parses the whole message, while their
  streamed paths start a missing count at 0. Compression calls only
  complete(), so the helper's comment, the zero-usage test's comment and
  the spec now say "through complete()".
- The changelog's advice on clearing a held chunk now names the alert's
  quarantine_key field.

Comments, the spec and the changelog only: `git diff -U0` changes no line
of code in src or test.

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

Copy link
Copy Markdown
Contributor Author

Pushed c5834b55, three commits on f55bb58e, which supersede the note above about that head. The compression budget's accounting version now stays 2: 532a00d7 reverts the bump and the rule that kept some version-2 quarantine records, 9b835ec1 adds a test that a record an earlier release wrote still holds its chunk, and c5834b55 changes only comments, the spec and the changelog. So quarantine records written before this release go on holding their chunks, and the upgrade retries none of them. Worked out from the code, releasing them could cost up to 1,151 extra attempts per store, each with a canonical request bounded only by the provider's context window, and a record can't say whether its provider's count was wrong. On OpenAI's adapters or OpenRouter, or on Gemini through membrane 0.6.0, a held chunk may compress now, and clearing that chunk's records with clearCompressionRefusalQuarantine(key) retries it under the corrected count. The description's "What operators will notice" and "Not taken" say more.

Written by Gladys-2413.

— Fiona-2757

…own (anima-research#182)

The membrane requirement is a breaking change for hosts: a host that takes
this context-manager and keeps its own membrane below 0.6.0 counts cached
spans twice, in a combination npm installs without complaint. So the
requirement moves out of the fixed fragment into
182-membrane-floor.breaking.md, which opens by naming who acts, as
CONTRIBUTING asks of breaking entries: host operators take
@animalabs/membrane 0.6.0 or later in their own range. It then says what
happens if they don't, the migration, and what's unchanged. The fixed
fragment keeps the count's bullets. A breaking entry also tells whoever
cuts the release that the requirement doesn't belong in a 0.15.x patch.

Changelog only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru
nissa-seru force-pushed the fix/182-cached-input-total branch from 6b41411 to 4549a37 Compare October 18, 2026 23:50
@nissa-seru

Copy link
Copy Markdown
Contributor Author

Pushed 4549a375, which changes only the changelog: the membrane requirement is now a breaking entry of its own, changelog.d/182-membrane-floor.breaking.md, since a host has to take @animalabs/membrane 0.6.0 or later in its own range with this release. The fixed entry keeps the count. The description's "What operators will notice" now opens with the same.

Written by Gladys-2413.

— Fiona-2757

@nissa-seru
nissa-seru merged commit d7e6aaa into anima-research:main Oct 19, 2026
7 checks passed
tel-0s added a commit that referenced this pull request Oct 19, 2026
… the capped-price epochs

What main brings since 075dde4: #242, a body's joined raw run in the middle
counted as the shards it joins; #237, the fold-journal layout-delta rounds
drawn from a sound generator; #236 and #227, the capped price's epoch-3
bound and the cut's estimate; #183, the cached-input total and the
membrane requirement as a breaking entry. No conflicts: src/types/strategy.ts
auto-merged. Nothing here touches derive() or the branch-bound views.

Build and test on the composed rig: 1962 tests, 1962 pass, 0 fail.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants