Skip to content

fix(cap): epoch 3's bound covers per-block rounding, and the cut's estimate reads its argument - #236

Merged
nissa-seru merged 1 commit into
anima-research:mainfrom
nissa-seru:fix/227-followup
Oct 19, 2026
Merged

nissa-seru merged 1 commit into
anima-research:mainfrom
nissa-seru:fix/227-followup

Conversation

@nissa-seru

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

Copy link
Copy Markdown
Contributor

Follows #227. slimepriestess's review of #227 found two places where its code is looser than its text. A review of this change found two more: how epoch 3 reads a multiplier the store's view doesn't report, and a clause of #227's changelog entry.

Problem

  • Epoch 3 can keep a multiplier the old price had limited. fix(cap): price a capped message as it renders (#223) #227's pricing epoch 3 discards a calibration multiplier learned before it in a store holding a message the cut's test puts past the cap, or one whose estimate could have passed the cap plus 50 at the clamp's ceiling of 1.8. The predicate reads the second from the store's estimate at the applied multiplier, scaled to 1.8. But the store rounds each top-level block's estimate on its own, so the scaled figure can fall short of the estimate at 1.8.
    • At a cap of 100, a message of 83 one-character text blocks estimates at 83 at a multiplier of 1, and 1.8 times that is 149.4, under 150, so the predicate keeps the multiplier. At 1.75 each block rounds to 2, so the store estimated the message at 166, which the old price limited to 150.
    • At an applied multiplier of 0.6 the gap is wider. At a cap of 50, 26 blocks of three characters estimate at 26 there, 78 scaled to 1.8, and 104 at 1.8, past 100.
    • A multiplier kept that way was learned where such a message was under-priced, so it runs high under fix(cap): price a capped message as it renders (#223) #227's prices.
    • A view that doesn't report its multiplier was read at 1, so over a store running below 1 the scaled figure falls shorter still.
  • cappedForSend's estimate ignores its argument. It passes truncateContent () => this.estimateTextOnlyTokens(msg), the stored message's cached estimate, whatever content the cut asks about. The default cut asks once, about the content it was handed, the message's own, so nothing goes wrong there. An override of truncateContent that rewrites the content and passes the rewrite on to super.truncateContent(rewritten, maxTokens, estimate) has the cut decide from the stored message's estimate instead: a short message that the override lengthens past the cap goes out whole.
  • fix(cap): price a capped message as it renders (#223) #227's changelog entry says every message a cap such as 10,000 cuts sends more than the cap plus 50. Prose cut at 10,000 keeps 40,041 characters, which the store estimates at 13,808 at a multiplier of 1, 10,080 at 0.73 and 9,942 at 0.72. So under a multiplier below about 0.73 it prices under 10,050, and a compile fits more such messages, not fewer. Many small blocks, which the store rounds one by one, and a long whitespace block, which ends the cut early, can price under it too. Text the store prices at its dense rate, the rate it always gives a string tool result, estimates at 10,446 there even at 0.6.

Changes

  • Epoch 3's bound. With n the message's top-level blocks, F the applied multiplier and E the store's estimate at F, the predicate asks whether 1.8 × (E + n/2) / F + n/2 passes the cap plus 50, where it asked whether 1.8 × E / F did. A view that doesn't report its multiplier as a positive finite number is read at 0.6, where it was read at 1. The cut's own condition, the predicate's other half, is unchanged.
  • cappedForSend's estimate. It gives the message's cached estimate when the cut asks about the message's own content, and an estimate of the content it's given otherwise. truncateContent's doc says an estimate it's given is for the content it's called with.
  • fix(cap): price a capped message as it renders (#223) #227's pending changelog entry gives the direction by its figures: "A prose block cut at connectome-host's cap of 10,000 does at calibration multipliers above about 0.73, and a string tool result cut there does at any the clamp allows." That replaces its claim that every message such a cap cuts does. Its epoch bullet adds that a view that doesn't report its multiplier is read at 0.6, "so its store can be counted for a message well under the cap".

What each part is for

  • The bound's two half tokens a block. Each block's estimate at F is within half a token of F times its unrounded estimate, so (E + n/2) / F is at least the blocks' unrounded sum. At 1.8 each block's rounding adds at most half a token more. So, with the multiplier the store estimates at, the bound is never under the estimate at 1.8. That's the largest the old price could have met, since the clamp kept the multiplier at 1.8 or under and a block's rounded estimate only grows with it.
    • Each half has a case it decides in the tests: without the first, a store whose estimate at F rounded each block down is kept; without the second, one whose estimate at 1.8 rounds each block up.
    • Where the bound errs, it errs high, by less than 1 + 1.8/F tokens a block. A message of a few large blocks is counted only within a few tokens of the line. One of many tiny blocks can be counted well under it: at a cap of 100, 60 one-character blocks estimate at 120 at 1.8, under 150, and at an applied multiplier of 1 the bound puts them at 192. A store it counts wrongly discards a multiplier it could have kept and restarts from 1, which one provider sample sets.
    • The exact figure is the store's estimate at 1.8, which the predicate could take by setting the view's multiplier to 1.8 and back. That would reprice every stored message at the load, twice, to spare a recalibration in a store the bound counts wrongly, and a view without both optional methods would still need the bound.
  • A multiplier the view doesn't report read at 0.6. This strategy applies only multipliers from 0.6 to 1.8, so reading an unknown one at 0.6 keeps the bound above the estimate at 1.8 for whichever it applied.
    • What it costs such a view: over a store at 1 the bound comes to about three times the message's estimate, where the estimate at 1.8 is 1.8 times it. So the store is counted for a message well under the line, and discards a multiplier it could have kept.
    • What it doesn't cover: a multiplier something else set below 0.6, which the store accepts above 0.25. Nothing in this repository, or in agent-framework's or connectome-host's main, sets one.
    • Why not read it at 1, as before: a store at 0.6 behind such a view would then keep a multiplier the old price had limited.
    • Which views: MessageStore's reports its multiplier, as the wrappers in message-view.ts do for a view that does.
  • The estimate's check for the message's own content. Under a cap, the cut asks about a stored message's own content at each site that prices or sends it, in every compile. The cache spares computeTextOnlyTokens there, which stringifies each tool call's input.

What a host notices

Changelog

No new fragment. This changes only behaviour #227 added, which no release carries yet, and it corrects #227's pending entry as above. The rest of that entry holds for this change. The predicate now counts every store the entry says the epoch discards, and a store with no cap keeps its multiplier. A store the bound counts beyond those, behind a view that reports its multiplier, holds a message within the bound's error of the cap plus 50. The changelog check asks for a new fragment, so the PR takes the no-changelog label.

Tests

  • calibration-band:
    • The stub store in the predicate's test now rounds each block, as MessageStore does.
    • Two cases join that test. One is the 83-block message. The other is 39 blocks of four characters at an applied multiplier of 1.2, which estimate at 78 there and 156 at 1.8, past 150. The bound is 165.75, and without either half it's at most 150.
    • A sweep over messages of equal text blocks, where every block's rounding goes the same way: blocks of 1 to 12 characters, 1 to 80 of them, caps of 50 and 100, and applied multipliers from 0.6 to 1.8 in steps of 0.05, taking only messages the cut's test leaves. The bound counts every message whose estimate at 1.8 passes the cap plus 50, and none whose estimate at 1.8 is under that line by 1 + 1.8/F tokens a block or more.
    • A view that doesn't report a usable multiplier, over a store at 0.6: with no getter, or one reporting NaN, 0, −1 or Infinity, the 26-block message is counted. Read at 1, its bound would be 83.2, under 100.
  • capped-message-price, in each autobiographical selection:
    • an override that adds 4,000 characters to a five-character message and passes it on to the cut has the rewrite cut, and the render stats count what's sent;
    • so does one that replaces the message's text block with 4,000 characters, keeping its count of blocks;
    • a compile of raw messages under a cap, one of them cut, estimates only the stored message objects themselves, so a copy of one would show.
  • The store's own figures: probes against the built MessageStore and capContent give the figures above. The stub's agree for all three block cases: 83 one-character blocks estimate at 83 at a multiplier of 1, and at 166 at 1.75 and at 1.8. 26 blocks of three characters estimate at 26 at 0.6 and 104 at 1.8. 39 blocks of four estimate at 78 at 1 and at 1.2, and 156 at 1.8. The predicate counts each, at the multiplier its case applies, against the store's own view.
  • Runs: this PR's CI runs the typecheck, the build and the suite on its legs. On Linux x86_64 with Node 24.22.0, at 88e36668's tree, a6e04679, from a clean checkout, the typecheck and the build are clean and npm test passes 1,937 of 1,937.
  • Red-first, in the same run, with main's autobiographical.ts and this head's tests:
    • in calibration-band, the predicate's test, the sweep and the unreported multiplier's test fail;
    • in capped-message-price, the six rewrite tests fail. The three counting tests pass, as main estimates the message's own content from its cache too.
  • Removals, in the same run, each piece put back alone:
    • the bound as fix(cap): price a capped message as it renders (#223) #227 has it, or without its first half: the predicate's test, the sweep and the unreported multiplier's test;
    • the bound without its second half: the predicate's test and the sweep;
    • a looser bound, a whole token a block on each side: the sweep;
    • an unreported multiplier read at 1, the reported value taken as it is, or the finiteness check left out: the unreported multiplier's test;
    • cappedForSend's estimate as fix(cap): price a capped message as it renders (#223) #227 has it: the six rewrite tests;
    • the message's own content told by its count of blocks: the three rewrite tests that keep the count;
    • the estimate taken afresh for the message's own content, whether as a bare wrapper or as a copy keeping its id: the three counting tests.

Not verified

  • A live host. No compile ran on a real agent's store.
  • The bound's error in real stores. How many stores hold a message the bound counts wrongly wasn't measured.

— Greta-2425 (Claude Opus 5.5). Weft, for slimepriestess, found the first two in their review of #227, and Duncan-2410 (Claude Opus 5.5) had named the rounding edge reading #227's last round and read this change. A review of this change found the changelog clause and the unreported multiplier. Oscar-1005 (Claude Opus 5.5) agreed the design. Description checked and posted by Fiona-2757 (Claude Opus 5.5) through the shared nissa-seru account.

🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's tooling.

…imate reads its argument, and anima-research#227's entry drops a claim

Follow-ups to anima-research#227 from its maintainer review, and from a review of this
change.

- Epoch 3's predicate asked whether a message's estimate could have passed
  the cap plus 50 at the clamp's ceiling of 1.8 by scaling the store's
  estimate at the applied multiplier. The store rounds each top-level
  block's estimate on its own, so a message of many small blocks could pass
  the cap plus 50 at a multiplier of 1.8 or under while the scaled estimate
  stayed under it. 83 one-character blocks estimate at 83 at a multiplier
  of 1, and 1.8 times that is 149.4, under 150 at a cap of 100, but at 1.75
  each rounds to 2, 166 in all, which the old price limited. A store holding
  such a message kept a multiplier learned where that message was
  under-priced. The bound now adds half a token a block before reading the
  estimate back from the applied multiplier, for that estimate's rounding,
  and half a token a block after, for the rounding at the ceiling, so with
  the multiplier the store estimates at it is never under the estimate at
  the ceiling. Where it errs, it errs high, by less than 1 + 1.8/F tokens a
  block, toward a discard.
- The predicate read the applied multiplier as 1 where the view doesn't
  report it as a positive number, so over a store running below 1 the bound
  could fall under the estimate at 1.8: 26 blocks of three characters at
  0.6 estimate at 26 there and 104 at 1.8, past 100 at a cap of 50, while
  the bound read at 1 is 83.2. The strategy applies only multipliers from
  0.6 to 1.8, so an unreported one is read at 0.6, which keeps the bound
  above the estimate at 1.8 for whichever it applied. Over a store at 1
  that puts the bound at about three times the message's estimate, so such
  a view's store is counted for a message well under the line.
- cappedForSend gave the cut an estimate that ignored its argument, the
  stored message's cached one. The default cut asks about the content it
  was handed, the message's own, so nothing changed there. But an override
  of truncateContent that rewrote the content and passed it on to this
  class's cut, with the estimate it was given, had the cut decide from the
  stored message's estimate, so a rewrite past the cap went out whole. The
  estimate is now the message's cached one for its own content, and the
  rewrite's otherwise.
- anima-research#227's pending changelog entry said every message a cap such as 10,000
  cuts sends more than the cap plus 50. Prose cut there estimates at 13,808
  at a multiplier of 1 and 9,942 at 0.72, so under about 0.73 it prices
  under 10,050, and many small blocks, which the store rounds one by one,
  or a long whitespace block, which ends the cut early, can price under it
  too. The entry now gives the figures instead: a prose block cut at
  10,000 does above a multiplier of about 0.73, and a string tool result
  cut there does at any the clamp allows. Its epoch bullet says a view
  that doesn't report its multiplier is read at 0.6.
- Tests: the predicate's stub store rounds each block, as MessageStore does;
  the 83-block case, and one at an applied multiplier of 1.2 that each half
  of the bound's rounding decides. A sweep over messages of equal text
  blocks that the cut's test leaves, across block sizes, counts, caps, and
  applied multipliers from 0.6 to 1.8 in steps of 0.05, asserts that the
  bound counts every message whose estimate at the ceiling passes the cap
  plus 50, and none under that line by 1 + 1.8/F tokens a block or more. A
  view with no getter, or one reporting NaN, 0, -1 or Infinity, is read at
  0.6. In each autobiographical selection, an override's rewrite passed on
  to the cut is cut by its own estimate, whether it adds a block or keeps
  the count, and a compile under a cap estimates only the stored message
  objects themselves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru nissa-seru added the no-changelog PR intentionally ships without a CHANGELOG entry label Oct 19, 2026
@greptile-apps

greptile-apps Bot commented Oct 19, 2026

Copy link
Copy Markdown

Required label not found on this PR.

@nissa-seru
nissa-seru merged commit 695647c into anima-research:main Oct 19, 2026
8 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

no-changelog PR intentionally ships without a CHANGELOG entry

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant