Skip to content

Merge main into the calibration fix: the observer's calibration records at its new outcomes - #3

Closed
nissa-seru wants to merge 91 commits into
Anarchid:fix/323-calibration-overheadfrom
nissa-seru:esther/cm324-merge-main
Closed

nissa-seru wants to merge 91 commits into
Anarchid:fix/323-calibration-overheadfrom
nissa-seru:esther/cm324-merge-main

Conversation

@nissa-seru

Copy link
Copy Markdown

To read only what this merge adds: git show --remerge-diff 066a6c8f. That shows the resolution of the one conflict, the outcome docs and the tests. GitHub's diff also shows everything main gained since this branch's base, 0.21.0 and 0.22.0 included. One more commit, 57643cbe, corrects three of #2's own texts.

This PR contains #2: it's built on #2's head, 7099a7b1, and #2 stays open as it was reviewed. If you'd rather merge main yourself, what this PR adds is the observer's record at each exit of the calibration path, below.

Problem

anima-research#335, merged as 50c28f7f, added an observer that a host passes to ContextManager.open. It's told of each calibration sample, from reportRealInputTokens' out-of-band path as it was before this branch. Merging main into this branch conflicts in that path in src/strategies/autobiographical.ts, since this branch restructures it into runs, and #2 adds the two-sample hold.

At git's own merge, taking this branch's side of the conflict, four of anima-research#335's observer tests fail, for two reasons:

One test fails for both reasons.

Changes

What each part is for, smallest first

Tests

Runs at 57643cbe on Linux, Node 24.22.1, from npm ci:

Removals: each piece this merge adds was removed or weakened in turn, 15 variants, with both test files run. Leaving 'deferred' out of the list fails tsc, and each of the other 14 fails a test:

  • a rejected out-of-band sample: not reported, or reported with the multiplier it implies as its ratio;
  • a deferred sample: not reported, reported as 'held', reported with the multiplier it implies as its multiplier or as its ratio, a provisional multiplier deferred like a learned one, the hold ended by a rejected declared sample, or the mark kept through an undeclared move;
  • a replacement: a run's, a pair's or a provisional multiplier's reported as 'stepped', a run's or a pair's result within the dead band reported as 'replaced', and a provisional multiplier's run there reported as 'held'.

Written by Esther-2584 (Claude Opus 5.5).

Read by Meg-2646 (Claude Opus 5.5).

Checked and opened 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.

Milo-2653 and others added 30 commits October 19, 2026 13:02
…ve window kept within them

A request carries at most so many images and so many image bytes. The
provider refuses one with more than 600 images and documents together,
and membrane refuses, before sending it, one whose serialized messages
pass its byte cap (28 MiB unless MEMBRANE_MAX_REQUEST_BYTES says
otherwise). The live window's limits are targets a compile keeps to, and
nothing bounded what a request could reach: a window configured as
unlimited (0), or above those limits, compiled requests the provider or
membrane would refuse, and a compression prompt had no count limit at
all. A caller that appends to a compiled request without recompiling,
such as a turn's continuation rounds, had no figure to stop at.

- maxRequestImages (default 500) and maxRequestImageBytes (default
  24 MiB) on AutobiographicalConfig and WindowedPassthroughOptions. 0 is
  no ceiling, and a value that isn't 0 or a finite number above it takes
  the default.
- The window's count and byte limits are clamped to them in one place,
  resolveLiveImageLimits, which both strategies' strip, the hysteresis
  watermark and the post-strip estimates read. An unlimited limit means
  up to the ceiling, a limit above it keeps the ceiling's worth, and a
  fractional limit or ceiling is floored, to at least 1.
- In the window, the ceilings bind what the window counts: top-level
  image blocks, by the length of their base64 source.data. An image
  nested in a tool result, a generated image and a data: URL's bytes
  aren't counted there, and a caller's compile-time injections and its
  appends are its own. maxLiveImages' doc says so once, and the other
  limits' docs refer to it.
- ContextStrategy.liveImageLimits() reports the clamped limits and the
  ceilings. Both strategies implement it.
- Compression prompts count every image, nested and generated ones
  included, against maxRequestImages, in the walk that keeps their byte
  budget: newest message first, and in block order within one. That
  budget stays within maxRequestImageBytes, and a budget that isn't a
  positive finite number means up to it. The log line names the count
  ceiling and the images kept.
- A strategy warns once, when it initializes, when a limit sits above its
  ceiling (the compression budget included) or a ceiling is malformed. It
  marks a setting the host didn't give as the default, and gives a byte
  figure in MiB when it's a whole number of them.
- Text: maxLiveImages' and windowed passthrough's docs no longer say 0 is
  unlimited or off, and maxLiveImages' doc says a window kept at the
  ceiling leaves an appending caller no room once it's full.
  maxLiveImageBytes' doc and the byte default's comment no longer promise
  membrane's whole-request cap or cite the API's 32 MB.

Tests: request-image-ceilings covers the accessor's defaults, clamps,
malformed and fractional values; each strategy's strip under the
ceilings, and the window counting top-level images only (a nested and a
generated image pass the count ceiling); the post-strip estimates under
both ceilings and after the clamped limit changes; the watermark under
both ceilings; the warning's cases; and the compression prompt's count
and budget. The image-stripping test of a disabled policy now disables
the ceilings too and carries more images than the default window, and
the compression log line's test takes its new wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under a window limit of 0, which meant no limit, or one above a ceiling,
the window keeps the ceiling's worth, and an operator who set no limit
had no line saying where one now applies: the initialize warning says
nothing for 0, since "as many as fit" is what 0 means.

- ceilingSetLimits says which of the window's limits a ceiling sets: a
  limit given as none or above its ceiling, where the ceiling is a
  limit.
- Each strategy says once for each ceiling, the first time an image
  falls past it under such a limit, through the same route as the
  initialize warning: which ceiling, its value, and the window limit it
  held (ceilingBoundLine), marking defaults as the warning does. The
  memory strategy says it from its strip and from its hysteresis
  watermark, whose trims strip below the ceiling.
- It changes no request: the strip keeps and replaces what it did.
- maxRequestImages' and maxRequestImageBytes' docs and the changed
  fragment say so.

Tests: for both strategies, the line once for a count of 0, one above
the ceiling, the default window under a lowered ceiling and a byte budget
of 0; nothing when the host's own limit strips, equals the ceiling, or
nothing falls past it; and for the memory strategy, the line from its
watermark under each ceiling.

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

From a review of the round before it:

- The ceiling line comes only from a render that's sent: a dry compile, a
  preview, an observer's reconstruction say nothing (_dryRunSelect), and
  the memory strategy's strip says it only when its selection asks
  (applyImageStripping's opts.sent, from selectAdaptive and
  selectHierarchical), so kv-unified's kept layout, rendered to compare
  against, doesn't. Windowed passthrough's dry render says nothing either.
  A preview's own ceiling no longer reads as the window's, or uses up the
  line.
- Its words hold for a single image bigger than the byte ceiling: "an
  image fell past maxRequestImageBytes ..., so images past it are now
  shown as placeholders".
- "(default)" marks only defaults in force: in the memory strategy, a
  value the library's default layer gave or no layer gave, and a value
  given as undefined or null where a default then applies, but not
  maxLiveImages, whose undefined reads as no limit; a value a subclass's
  enforced layer gives isn't a default. A malformed ceiling, which takes
  its default, is marked in both lines.
- Docs: the byte ceiling's margin names images the window doesn't count,
  and maxRequestImageBytes' doc qualifies when the line is said.

Tests: the line said at the first compile that strips, with the
strategy's name and through the warning's route; once for each ceiling
when both strip; nothing when a host's byte limit strips, from a dry
compile, from a preview with its own ceiling, or from the watermark when
the host's own limit binds; the adaptive selection's line; nullish and
enforced settings' marks; the line's words for default and malformed
ceilings and byte figures; compression budgets that are negative, NaN or
fractional; the default compression budget's warning clause alone; and
the returned requests of the compression walks.

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

From a review of the round before it:

- The memory strategy reports the ceiling line once a live select returns
  its render (reportCeilingHits), not from inside the strip: a select
  refused after its strip, such as one whose structural repair drops the
  newest turn, says nothing, and the next select it returns says it.
  The selection's strip records what its ceilings stripped for that
  render (`sent`), and a hysteresis watermark moved by a ceiling outside
  a dry run, as on a new message, leaves it for the next render returned.
- The accessor's doc says its figures count the images the window counts.

Tests: a refused select says nothing and the next one says it; a
preview's own ceiling moving the watermark leaves no line; a single image
bigger than the byte ceiling; the dry compile, with what it kept, and
the compare-only strip for the byte ceiling too; both ceilings each at
its own step; and the warning's marks for an undefined byte budget and a
malformed byte ceiling under a limit above it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ept with its branch

From a review of the round before it:

- The memory strategy's live select clears what earlier strips recorded
  before its own strips run. A dry select's strips record too, and a strip
  with no limits returns before recording, so a host with every limit off
  heard the line at its next compile after a preview with a ceiling of its
  own.
- A ceiling's move of the hysteresis watermark is cleared with the
  watermark itself when a branch loads (clearBranchMirrors), so a move on
  one branch isn't reported at the first render returned on another.
  reportCeilingHits' doc says the move it reports is this instance's, on
  this branch: a watermark loaded from the store carries no record of what
  moved it.
- The ceilings' docs, LiveImageLimits' and the changelog fragment say the
  window keeps the images it counts within the ceilings, and the 500
  default's margin names what the window doesn't count.

Tests: a preview at a ceiling of its own, then a compile with no limits,
says nothing; a watermark a new message moves says nothing until a compile
returns, which says it once; a watermark moved on a fork says nothing back
on main; the preview's own watermark trim is checked; the refused select
is matched by its error; and the step where both ceilings strip in turn
expects the byte walk's greedy keep, as on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…only when a ceiling decides

Answers review round 1 on anima-research#268: R1-1, R1-2 and R1-4 to R1-9.

R1-1: maxRequestImages and maxRequestImageBytes become imageCeiling and
imageByteCeiling. Each doc opens with what it bounds: the top-level image
blocks a strategy's live window keeps, and what a compression prompt
carries. Each then says what it doesn't count: the window's nested and
generated images, and whatever a caller injects or appends. The module's
constants follow (DEFAULT_IMAGE_CEILING, DEFAULT_IMAGE_BYTE_CEILING).

R1-2: ContextStrategy.liveImageLimits() becomes imageCeilings(), which
reports the two ceilings alone. The window's own target leaves the public
API (ResolvedImageLimits, inside the module), and the doc says the
ceilings are configuration, not usage: a caller that wants a request
within them counts that request's images itself.

R1-4: the once-only line is said only when a ceiling decides that an
image is stripped (ceilingsDeciding), not when the depth limit, the
hysteresis watermark, or a count or byte limit the host set strips it
anyway. That holds in both strategies' strips and in the watermark's move.
"sent" becomes "returned", since a strategy sees the render select
returns, not whether it was sent.

R1-5: a breaking fragment names the change in what 0 means for
maxLiveImages, maxLiveImageBytes and maxCompressionImageBytes, with the
migration, the siblings' ranges on npm, and what doesn't change.

R1-6: both ceilings are in DEFAULT_AUTOBIOGRAPHICAL_CONFIG, so
config:effective reports them.

R1-7: windowed passthrough's applyImageStripping takes `returned` with a
default of false, so a caller passing three arguments, as renderAsNewest
does on main, stays correct and says nothing.

R1-8: a compression test where a newer generated_image takes the last
slot from an older image.

R1-9: the note on membrane's byte cap says where it's checked:
transformRequest, which a call's first request and each native tool-loop
round go through but an XML-mode continuation round doesn't, and the
Anthropic adapter, on every request it sends (membrane 0.5.87 and 0.6.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One conflict, in src/strategies/autobiographical.ts: anima-research#249 wraps the live
select's selectPath in withToolPruning, and this branch assigns its result
to entries. Both are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s it through cappedForSend

Since tool pruning is priced as sent, the tool pass a compile runs between
the cut and the image pass (postProcessToolEntries) only repairs pairing,
and what sending prunes from a message's tool blocks is applied in
cappedForSend, which renderAsNewest runs for each stored copy. Its doc
said the pass it leaves out also prunes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t newer; the docs say where membrane checks

The memory strategy's hysteresis watermark credited a ceiling with a move
whenever the first image past a limit passed that ceiling and wasn't past
the depth by the watermark's own measure, which counts only newer
messages. The strips count the image's own message too, so a move the
depth limit would make anyway, to the same place, could say the line, and
a ceiling whose trim landed the watermark newer than the depth's could be
missed. The watermark now works out where it would land with only the
limits the host has in force, and credits each ceiling whose limit its
walks passed when its own landing is newer.

The docs say where membrane checks its byte cap, and that a compression
request, which sets shedOversizeImages, has its oldest images shed rather
than being refused. They say the line comes from a render a select
returns, that a fraction is floored to at least 1, and what the window
and a compression prompt each count. The breaking fragment's Unchanged
bullet names the compression count cap and the fractional floor, and its
account of a negative, Infinity or NaN limit is per value.

Tests: the watermark at the depth boundary, with and without a move the
depth would make anyway; a byte ceiling whose trim lands it newer; the
kind credited when the host's own count limit misfits first; a trim that
stops at the depth; the branch test under the byte ceiling; renderAsNewest
under a binding ceiling, for both strategies; the watermark read where a
test says it moved; and imageCeilings typed through ContextStrategy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wn byte limit

The byte guard's mirror of the count guard's test: a host byte limit of 40
in force misfits the oldest image first, and the count ceiling of 4 shows
up only in the trim, which lands newer. Without the byte guard, the line
names the default byte ceiling as well, which held nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ling alone stripped, in the render returned

The memory strategy's hysteresis watermark said the ceiling line at the
next render a select returned after a ceiling decided its move, whether or
not that render showed an image the move stripped. It credited a ceiling
whose trim, at ratio x the ceiling, decided the landing though no image
passed the ceiling itself, and it measured the host's own depth limit the
watermark's way, newer messages only, while the strips count an image's
own message too.

A live move now records the sequences it stripped that the limits the host
has in force wouldn't have: after where those limits would land it, through
where it lands, with the ceilings the first image past a limit passed, which
brought the move on. A later live move cuts earlier records at its own
host-only landing, since the host's limits strip those images anyway. The
strips credit a record's ceilings for an image in it that the depth limit
doesn't strip, and the select reports them for the render it returns, then
drops the records. So a move a host limit brings on says nothing even where
a ceiling's trim lands it newer, and the line's words, that an image fell
past the ceiling, hold.

The guard that a landing is above the watermark is gone: both walks stop at
it, and the trim's caps are no larger than the first walk's.

The docs say the line comes at the first render a select returns where
images are placeholders because an image passed the ceiling, that a
fraction of maxLiveImages is floored to at least 1, and that membrane's
shedding stops when the request fits or no image is left, after which only
the Anthropic adapter's check refuses one still over its cap.

Tests: a move the depth limit, or the host's own count or byte limit,
brings on says nothing where a ceiling's trim lands it newer; an image the
move strips that the strip's depth also strips says nothing; the host's own
limit landing the move where a ceiling does says nothing, for each kind; a
later host move overtaking a ceiling's before a render says nothing; and
only the ceiling an image passed is named when both limits are the
ceilings'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ds is said

Dropping the records at every reported render lost the line when that
render left the move's images out, such as an adaptive selection at a
smaller budget, and a later render showed them. A record now stays until
each ceiling it holds has been said, and goes once a later move's cut
empties it, which bounds them as the clear did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… host's own limit strips anyway

The strips credited a ceiling's move for any image in its record that the
depth limit kept, without the check of the host's in-force count and byte
limits that the strip applies to an image past a ceiling itself. So an
image a later message pushed past the host's own limit, below the
watermark where the next move's walk can't see it, still brought the line.
The strips now credit a move's record for an image only when it passes no
limit the host has in force, counted as the strip counts it.

The line ends "so older images are now shown as placeholders": a
watermark's trim strips images inside the ceiling too, down to ratio x the
ceiling. ceilingsDeciding's doc says a ceiling's move is credited from its
record, and the landing's comment no longer claims the trim's caps are
always tighter, since a floored cap of 0 is no limit; a landing is above
the watermark because both walks stop there.

Tests: a ceiling-brought move whose host-only landing is defined and older
keeps the line for the images between, for each kind; and an image the
host's own byte limit strips by render time says nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iling's watermark move too

The check of the host's in-force limits before crediting a ceiling's move
had a render-time test for its byte half only. A byte ceiling's move lands
on the oldest image; two more images come before the compile, and in the
render the oldest is the fifth, past the host's count of 4, so nothing is
said. The comment on the advance now says the strips report such an image
in a render a select returns, not the next one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ded watermark's silence

From our internal review, text only:
- The line ends "so some images are now shown as placeholders". The greedy
  byte strip can replace the newest image and keep older ones, so "older
  images" named the wrong ones. Its five test strings follow.
- imageByteCeiling's doc says where membrane checks a request against its
  cap: an XML-mode continuation round isn't checked there, and of the
  provider adapters only Anthropic's checks again, as
  src/live-image-limits.ts already says.
- imageCeiling's doc, imageByteCeiling's doc and the changed and breaking
  fragments say the line isn't said for an image the host's own limits
  would strip, or for what a watermark loaded from the store strips, which
  carries no record of what moved it.
- The breaking fragment's Siblings line names agent-framework 0.23.2 and
  connectome-host 0.11.0, the versions now released, whose ranges are still
  ^0.14.1 and ^0.14.0 (npm view, 2026-10-21).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main gained anima-research#280, anima-research#284, anima-research#272, anima-research#286 and anima-research#287 since 776391d. Some of them
touch src/strategies/autobiographical.ts and src/types/strategy.ts, which
this change edits, in other places, and git merged without a conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main gained anima-research#292, anima-research#289, anima-research#296, the 0.18.0 release, anima-research#293 and anima-research#219 since
17a7f6d. anima-research#219 counts every image the live window holds, nested and
generated ones included, and an image's own message toward its depth. It
conflicts with this change in three files:

- src/types/strategy.ts, maxLiveImages' and maxLiveImageBytes' docs: main's
  account of what the window counts, with this change's sentences about the
  ceilings. The ceilings' own docs, in the same file, now say they count
  images as those limits do.
- src/strategies/windowed-passthrough.ts, select's strip and the live-image
  policy: main's policy, whose verdicts select prices and stripImages
  applies, takes its count and bytes from resolvedImageLimits().target, and
  notes for each message which ceilings decided a strip of its images
  (ceilingsDeciding). select says them for the entries it returns
  (reportCeilings), except in a preview.
- src/strategies/autobiographical.ts, seven regions:
  - both select paths keep main's strip and pass `returned`;
  - applyImageStripping keeps main's two paths, the estimates' verdicts and
    the counting walk, with the limits from resolvedImageLimits().target.
    Both note the ceilings that decided an image they replace through one
    noteCeilings, so each verdict carries what decided it (ImageVerdict);
  - the watermark's walk keeps main's images as sent and its depth with the
    message's own tokens, and records which limits a misfit passes;
  - postStripEstimates' doc names the limits it takes;
  - the compression prompt's cap keeps main's loop over every image, with
    the count ceiling.

Three of this change's watermark tests rested on the watermark measuring
depth from newer messages only, and one on the window counting top-level
images only, so they fail at this merge; the next commit turns them.

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

The live window now counts every image, nested and generated ones
included, and an image's own message toward its depth (anima-research#219). So:

- live-image-limits.ts' account and the added and changed fragments say the
  ceilings bound the images the window counts, as its limits count them,
  and that an image inside an opaque provider item isn't counted.
- The breaking fragment's sibling line names agent-framework 0.24.0 and
  connectome-host 0.12.0, which depend on ^0.18.0.
- The test that pinned a nested and a generated image passing the count
  ceiling now pins the ceiling binding them.
- Three watermark tests had rested on the watermark measuring depth from
  newer messages only. Each is rebuilt on the same rule with the depth
  counted as anima-research#219 counts it: the ceiling's line when only the ceiling moves
  the watermark, the quiet when the depth limit brings the move on, and the
  quiet for an image a ceiling's move stripped that later messages took
  past the depth.

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

The policy's guard around noting them only saved an object per image
message, and no test could tell it was there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…firstChanged on compile results

membrane 0.7.0's system_change block (SystemChangeContent) changes the tools
the model is offered, and its system text, from a place in the conversation
on. context-manager now carries a message of such blocks: priced, alone in
its message, compiled unaltered in its place under every strategy, never
dropped, and kept out of compression. The membrane floor rises to ^0.7.0,
the release that carries the block.

- Kept in place. The autobiographical strategy (and KnowledgeStrategy)
  keeps a message of changes raw at resolvedPinBounds, the one site the
  chunker, the adaptive picker and its solvers, and the hierarchical
  emission read. After a head-window reset, the changes stored before the
  head are part of the head, ahead of the head window, in order
  (headPositions), on the hierarchical and adaptive paths and in
  kv-unified's cache-check rebuild, so a removal never runs before the
  definition it removes. The middle's ranges come from one function
  (middleRanges), which also ends main's overlap with the head window
  disabled after a reset.
- KnowledgeStrategy's chunker closes a chunk at a change between two of its
  compressible messages, and where the L1 summary covering its messages
  changes, except between a call and the result right after it, where
  closing would let pairing repair drop the result; that chunk is refused
  by the overlap guard and both are sent raw, as on main.
- The passthrough strategies send the changes before their window ahead of
  it, in order, and count them first; windowed's re-anchor target is the
  changes plus reAnchorFraction of the room they leave, and its fallback
  anchors at the newest message that isn't a change. Windowed's refusal
  names one of three stages by what its numbers hold: the newest message
  alone, its system changes and the newest conversation message, or its
  system changes alone. Its anchor moves only for a compile that sends.
- None is dropped. Changes alone over the budget are refused by every
  strategy with OverBudgetError and clearedBy empty, since only the caller
  consolidating them clears it. The adaptive plan refusal names compression
  only while the head, the tail and the changes between them fit, and its
  head/tail floor takes the newest conversation message past any trailing
  changes. PassthroughStrategy refuses when the newest conversation message
  doesn't fit beside the changes, naming newer-message while they leave
  room for one.
- Kept out of compression: every summarizer request leaves changes out, as
  it strips thinking, and merging messages for the request passes over a
  message left empty, so no empty turn or separator marks where it sat. A
  head-window reset's transition summary leaves out a message of changes
  alone.
- Priced from its JSON in MessageStore and ContextLog; refused at a write
  when beside other blocks, inside a tool result, or malformed (each field,
  element and definition field named by its place; a definition needs an
  object inputSchema of type 'object'); left out at a read, with a log line.
- Render stats count the head as a compile sends it, the changes before a
  reset head included; the structural repair's check takes the newest
  message that isn't a change as the newest turn; the coverage check names
  an unrendered change before a reset head as the head's.
- CompileResult.firstChanged: on a committing compile, the index of the
  first message, injections included, whose participant or content bytes
  differ from the last committing compile's, or null for the first. It's
  measured after injections, recorded last, and keeps no copy of stored
  content.

Tests: test/system-changes.test.ts (48 tests) and one in
test/kv-unified-render-state.test.ts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…re firstChanged when read; find changes once per listing

Answers the automated review of bbca9e3.

- A write that would put a system change between a tool call and a
  result that answers it is refused (MessageStore.refuseToolPairSplit):
  a message of changes while one of the nearest calls before it has no
  result between it and the change, a tool result answering a call that
  a change already follows, and an edit giving a message a tool_use that
  a change follows before its results. It reads back from the write over
  changes and over messages holding results but no call, to the nearest
  message holding a call, reading each message whole, every shard of its
  body, since ingress sharding puts a message's calls and results on its
  first shard. It checks that message's calls by id: a result in a later
  message answers one, as parallel calls' results in messages of their
  own do, and so does the message's own later result, as in a bundled
  cycle (splitMixedToolMessages). It stops at a message of any other
  kind, so a call no result answered further back refuses nothing; the
  refusal says to store a result for a call that will get none, or
  remove the call. MessageStore.append and edit check it, and
  ContextManager.addMessage checks first, so a refused add releases no
  expired hold. The API takes a system message with content only after a
  user turn, or an assistant turn ending in a server tool result, so
  membrane sends a change stored there after the result anyway. Stored
  between them, a change parted them: the base chunker and
  KnowledgeStrategy close a chunk at a kept-raw change, so the call and
  its result were folded into different summaries, and once the call's
  chunk was summarized pairing repair dropped the result. What a write
  can't see is left as on main, and its doc says so: a message of
  another kind between a call and its result, a call in an earlier
  message than the nearest one holding a call, a shape that exists only
  in a view, filtered or merged from several stores, and a removal or
  edit that takes away a result or a message between. The adaptive test
  of a structural repair after a reset at a trailing change reaches its
  store through a removal.
- CompileResult.firstChanged is compared when read, through an accessor.
  A committing compile still records its keys (references, and an
  injection's bytes, taken then since a caller may change its own
  array), but a caller that never reads the field serializes nothing
  else. The first read, or an assignment, settles the value and lets go
  of both compiles' keys; it stays an accessor, so a frozen or sealed
  result reads it too. Read after a later compile, it still compares its
  compile with the one before it.
- systemChangePositions remembers its answer per listing (a WeakMap on
  the store's array, which callers never change and a write replaces),
  since a select reads it several times.
- WindowedPassthroughStrategy.renderAsNewest gives a system change
  before the anchor as select sends it, ahead of the window, where it
  gave null: setAnchor can move the anchor past a message just stored.
- Fragments: the membrane floor's raise to ^0.7.0 gets a breaking entry,
  as 0.16.0's raise to ^0.6.0 had; the KnowledgeStrategy summary-span
  fix joins the fixed fragment; the added fragment names the refusal and
  that firstChanged is compared when read.

Tests in system-changes.test.ts: the refusal through the manager before
an expired hold is released and through the store, and a change taken
after the result; parallel calls answered in messages of their own; a
call on a sharded body's first shard; a sharded results message read
whole walking back from a change and forward from an edited call; a
change after a bundled cycle and edits giving one, a bundled message's
call that none of its later blocks answers left open, a plain message
and the store's end ending the forward walk; a call further back than a
message of another kind refusing nothing; a tool result answering a call
a change follows, as a removal leaves them, and another change there;
edits from either side, through the manager and the store;
renderAsNewest for a change before windowed's anchor; a listing's
changes found once; firstChanged compared when read, serializing nothing
on a read of the same arrays, read first after a later compile, carried
by a copy and JSON, read on a frozen result, taking an assignment, and
leaving out system injections. Two existing tests now check what they
name: the Knowledge call/result test asserts the compiled blocks, not
provenance ids, and the pinned-summary test checks its subject chunk
exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in main since 48bb87a: the combined-pair grace fix, the
stateless image strip, and the lone-surrogate store fix. The one
conflict is autobiographical.ts's import block, where both sides
added an import: system-changes.js here and stored-json.js on main.
Both are kept. This branch adds no JSON-bytes state write of its
own, so nothing here needs the new storedJson helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anima-research#311 removed getRenderStats()'s fallback (reconstructRenderStats): render
stats are null until a live compile. This branch had changed that fallback
to count a reset head's system changes and to start the tail where the head
ends; with the fallback gone, both changes go with it.

- autobiographical.ts: take main's getRenderStats (return
  this._lastRenderStats); reconstructRenderStats is gone. headPositions stays,
  used by the compile paths.
- test/system-changes.test.ts: the reset-head test asserts null stats before
  the compile and keeps its assertion on the compile's own stats; the test
  "render stats before a first compile count no message in both the head and
  the tail" is removed, since nothing renders stats before a compile now.
- changelog.d/system-changes.fixed.md: its bullet about getRenderStats()
  before the first compile is removed for the same reason.

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

antra-tess cut 0.19.0 from main (8d91b2f raised membrane to ^0.6.1, then
570303e chore(release): 0.19.0). Two conflicts:

- package.json: version 0.19.0 from main; the membrane range stays this
  branch's ^0.7.0, the floor the system_change blocks need.
- package-lock.json: main's, regenerated with
  `npm install --package-lock-only` against ^0.7.0; it now resolves
  membrane 0.7.1.

This branch's changelog.d fragments (system-changes.*) stay unreleased; the
0.19.0 section main folded into CHANGELOG.md is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
context-manager 0.19.0 raised its floor to ^0.6.1 for antra-tess/membrane#168's backport: on
0.6.1 a round a provider ends with `stop_reason: end_turn` while holding
complete tool calls runs as a `tool_use` round. membrane 0.7.0 doesn't have
that fix (v0.6.1 isn't an ancestor of v0.7.0; antra-tess/membrane#168 shipped in 0.7.1), so
`^0.7.0` admitted a membrane that drops it. The summarizer path declares the
agent's live tools, so on 0.7.0 a draft returned in a tool call under
end_turn would be lost.

- package.json: @animalabs/membrane ^0.7.0 -> ^0.7.1; the lockfile's root
  line follows (regenerated with --package-lock-only
  --ignore-scripts); it still resolves 0.7.1.
- changelog.d/system-changes.breaking.md: the floor and migration name
  0.7.1, with the reason.

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

0.20.0 raised the membrane floor to ^0.7.1 on main, the same range this
branch already asks, so package.json and package-lock.json now match main.

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

This branch no longer changes context-manager's membrane range: 0.20.0
already requires ^0.7.1, with its own Breaking entry for hosts. The
fragment held only that floor, so it goes; the added and fixed fragments
stay.

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

One conflict, in src/types/context.ts: this branch added CompileResult.
firstChanged where main added CompileResult.renderStats. Both fields are
kept, firstChanged first. context-manager.ts and the strategy merged
without conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ceiling line now credits the hysteresis rule's set by replaying it.
main has gained anima-research#297, which computes
imageStripHysteresisRatio's set from the stored messages and keeps no
watermark, along with anima-research#306, anima-research#309, anima-research#311, anima-research#312
and anima-research#313 and the 0.19.0 and 0.20.0 releases. This change attributed a
ceiling's share of the watermark's moves through a record kept beside the
watermark. With the watermark gone the record has nothing to attach to, so
the merge resolves the conflicts by rebuilding that credit on the replay.

Conflicts, all in src/strategies/autobiographical.ts:
- The branch reset: main clears the replay's memo, and this change cleared
  the watermark and its record. The merge takes main's line; the record is
  gone.
- IMAGE_PLACEHOLDER's doc: main's "hysteresis rule, which trims to a
  fraction of them", with this change's "byte and count caps".
- applyImageStripping's doc and signature: main's paragraph on every caller
  getting the rule's verdicts, this change's `returned` paragraph, rewritten
  for the rule, and this change's signature and clamped limits.
- The verdict branch: main's `asEstimated || ratio` condition. Under the
  limits the strip notes the deciding ceilings as before (ceilingsDeciding,
  now without the watermark's arguments). Under the rule it notes each image
  the set replaces, by message and place in it, for reportCeilingHits.
- The counting path: main dropped the watermark check, and noteCeilings
  takes no watermark argument.
- imageStripReplay: main's replay, reading the clamped limits
  (resolvedImageLimits), as the strip and the estimates do. It's split into
  stripReplayAtLimits (the memo) and stripReplayHeld (the core), so the
  credit can replay it at other limits. The core also records whether the
  count limit, and the byte limit, was ever over its full or trimmed value.
  This change's watermark walk and its record went with the watermark.
- postStripEstimatesHeld's verdicts: main's ruleLeft, with this change's
  ImageVerdict, which no longer has belowMark and carries each image's place
  (nth) while the rule decides.

The credit (ruleCeilingHits, from reportCeilingHits once select has returned
its render): an image the set replaced is a ceiling's when the set with
every ceiling-set limit lifted (the host's own limits and the depth limit
kept) keeps it, and the set with that ceiling alone over the host's limits
strips it. That mirrors ceilingsDeciding's two conditions. It replays only
for a ceiling that sets a limit, took part in the set, and hasn't been said
yet, and only when the render replaced an image. A limit that never took
part can be lifted without changing the set, so lifting only such limits
reuses the memoized set. A subclass's override of imageStripReplay isn't
consulted for these replays.

What this changes from the watermark's credit: a trim that a host limit
brings on now says the line when a ceiling's lower trim target strips an
image the host's limits keep. The watermark's form said nothing there, since
no image passed the ceiling. So the line no longer says "an image fell past"
one. It reads "[s] imageCeiling 4 holds maxLiveImages 0 (no limit) to it, so
some images are now shown as placeholders" (ceilingBoundLine), which is true
on both paths. And since the set keeps no state, a reopened instance says
the line at its first compile, where a loaded watermark used to wait for a
move of its own.

Tests (test/request-image-ceilings.test.ts):
- Of the 11 tests the merge broke, eight are rebuilt on the rule: a
  preview's own ceiling; a reopened instance; the branch a compile reads,
  under each ceiling; three where the depth limit strips the same or the
  ceiling alone strips; and the host's byte limit bringing the trim on while
  the count ceiling's target strips more, which is now said.
- Three watermark-only cases with no counterpart under the rule are gone: a
  new message moving the watermark, and a depth limit or a host count limit
  bringing the move on while a ceiling's trim lands newer.
- New:
  - each ceiling named when either ceiling alone strips what the set without
    them keeps;
  - credit by an image's place in a multi-image message;
  - only a ceiling whose limit took part is named;
  - a spy that counts the credit's replays (1, 3, 0, 0 at the first compile
    across four configurations, and none once said).
- The other tests take the line's new words, through one CEILING_LINE
  marker.
- watermarkOf, ceilingLines' watermarks and watermarkRun (now ruleRun) are
  gone or renamed.
The line a ceiling says is now credited by the strip under the limits, or
by replaying the hysteresis rule's set without the ceilings, and reads
"<ceiling> <value> holds <limit> <value> to it". The texts that still
described the watermark's record follow:

- imageCeiling's doc says when the line comes: an image past the ceiling
  that the host's own limits, the depth limit included, don't strip anyway,
  or, with imageStripHysteresisRatio, an image the set strips that the set
  without the ceilings keeps and the set with this ceiling alone strips.
  imageByteCeiling's says it does so as for imageCeiling.
- The breaking fragment says the line comes where images are shown as
  placeholders because of a ceiling, and no longer names a loaded
  watermark. Its siblings bullet takes today's ranges: agent-framework
  0.26.0 and connectome-host 0.13.0 depend on ^0.20.0 (npm, 2026-10-23).
- The changed fragment names the rule's set among the places that read the
  clamped limits, and says when the line comes under the limits and under
  the rule.
Oscar-1005 and others added 27 commits October 23, 2026 21:40
…of system changes

main's over-budget refusal (rawPinsDiagnostics) totals the middle's
positions in forceRawPinPositions. On this branch that set holds every
message of system changes too, since resolvedPinBounds keeps them raw as
a pin keeps a message. So a change in the middle was counted as tokens
the pins keep raw, though unpinning frees none of it: in the new test,
a 6,000-word mark's refusal reported 23,648 tokens against the mark's
own 18,241.

The total now counts only positions a pin keeps raw that aren't changes.
The new test fails without the fix (receipt alongside the branch's other
runs) and passes with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…can stop compression

A message no chunk held before its pin is never summarized while the pin
stands, and later compression requests carry it whole without pricing it
against compressionContextBudgetTokens, as for a pinRange over the same
parts (anima-research#332). Since whole-message pins, a mark made on a fresh long
message is one, so the release that carries them should say what happens
when such marks outgrow that budget, and that unpinning clears it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eaves out; a pin over a change counts nothing for it

rawPinsDiagnostics' doc says the positions it counts are those pins keep
raw less the messages of system changes. The test now also pins the
per-pin side: a range over the mark's message and the change beside it
is named with the mark's tokens alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'t hold

From the review of the first wording: a mark made as a long message
arrives can find parts already chunked; before the head window's end a
pinned message is charged and the fallback leaves it out; the source-only
modes keep compression going; another pin's level can still let the parts
fold; and a chunk the refusals quarantined can stay quarantined after the
marks are unpinned, until clearCompressionRefusalQuarantine. The note now
says each.

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

docs(changelog): note that marks larger than the summarizer's budget can stop compression
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…inding

feat(thinking-binding): leave out thinking a binding model can no longer take (anima-research#155)
main gained anima-research#330, anima-research#333 and anima-research#331 (the
thinking binding) and the 0.21.0 release. Four conflicts, each where both
sides add beside each other:

- src/index.ts and src/types/index.ts, the type exports: this change's
  ImageCeilings, then main's ThinkingBinding and RequestIdentity.
- src/types/strategy.ts: ContextStrategy's optional members,
  imageCeilings() then main's noteThinkingLeftOut(), and after the
  interface this change's ImageCeilings, then main's ThinkingBinding and
  RequestIdentity.
- src/strategies/autobiographical.ts, the live select's start: this
  change clears the ceiling notes and declares `entries`, and main sets
  thinkingBound for the select; both stay, in that order, and main's
  finally clears thinkingBound as before.
agent-framework 0.26.1 and connectome-host 0.13.0 depend on ^0.20.0 (npm,
2026-10-24), which admits neither 0.21.0, released since, nor a later
release that would carry this change.
One conflict, in src/context-manager.ts, where each side added to the end
of a committing compile and a private method beside the other's:

- The committing block now does both: firstChanged's record first, then
  the binding record's write and the strategy's note, each of which logs
  rather than throws. So a compile that throws before that block is still
  never the one the next is compared with.
- recordFirstChanged and bindThinkingIn sit side by side.

One meet the conflict didn't show: a binding compile sends an injection
that holds thinking as a new array of the caller's blocks, so telling
injections apart by array identity missed it. Its key then took no bytes
at compile time, and a block the caller changed in place before the next
compile read changed on both sides, which hid the change from
firstChanged. recordFirstChanged now tells an injection by its source in
the compile's provenance, which stays one to one with the messages. A new
test in test/system-changes.test.ts fails with the identity check
(firstChanged 4, expected 3) and passes with this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
connectome-host 0.13.1, released since the previous commit, depends on
^0.20.1, which admits neither 0.21.0 nor a later release that would carry
this change, as agent-framework 0.26.1's ^0.20.0 doesn't (npm,
2026-10-24).
Under imageStripHysteresisRatio the line credited an image the rule's set
replaced by replaying the set counterfactually: with the ceilings' limits
lifted, and with each ceiling alone. Once a ceiling's limit had taken part
anywhere in the history, every later compile whose render replaced an
image ran those replays, and where the images were ones the depth or the
host's own limit strips, nothing was credited, the line stayed unsaid, and
the next compile replayed again, for the life of the instance (review
round 2, R2-1, with a probe of one extra replay per compile).

The set's own replay now records, for each image it strips, which of the
window's full limits were over where the trim that stripped it began, and
the credit applies ceilingsDeciding to that: a ceiling decided the images
a trim strips when the limit it sets was over there and neither the depth
limit nor a limit the host set was. The record lives in the memoized set,
so reporting replays nothing. `involved`, the shortcut that lifted only
uninvolved limits, the counterfactual replays and the `wanted` filter
(reportCeilingHits already skips a said line) go, and stripReplayHeld is
folded back into the memoized entry.

Judged at the trim alone, the credit has two consequences the option's
documentation and the changed fragment now state: a trim a limit the host
set, or the depth limit, also brought on says nothing, though the
ceiling's lower target there can strip an image the host's own limits
keep; and the line is said for an image the ceiling's trim stripped even
where a later trim would have stripped it anyway.

Tests: the replay-count tests give way to one that reporting runs no
replay; the mixed-trim and later-trim cases are turned round; the place
test credits by place in both orders; a trim the depth limit brings on
with the count ceiling says nothing; and test comments that explained a
verdict by the counterfactual now explain it by the trim.
capCompressionImageBytes returned before filling its stats when neither
the byte budget nor the count cap was set, so prepareCompressionRequest
reported imagesKept 0 and imageBytesKept 0 while it kept every image
(review round 2, R2-6). The walk now runs with no cap too, keeping every
image and counting it; the 501-image test asserts the counts.
…siblings line is undated (R2-3, R2-5)

R2-3: membrane sheds and checks the messages the formatter built, and its
shed replaces only membrane image blocks, top-level or in a tool_result. A
request that sets shedOversizeImages and is built by the openai-responses
formatter carries input_image parts, so none is shed and the transform
doesn't check it against the cap, and that adapter doesn't check it again
(membrane 0.7.1: transformRequest, shedImagesToFitByteBudget, the
openai-responses formatter). The byte ceiling's doc and the byte-cap note
say so.

R2-5: the breaking fragment's siblings line no longer names versions and a
date, which went stale when agent-framework 0.26.2 was published on the
same range: no sibling's current range admits this release, and each takes
these changes on when it raises its range.
…ithout attributing each image

The once-only line that names a ceiling holding a limit of 0, or above it,
was said only for images the ceiling decided: under the limits, an image
past the ceiling that nothing else strips (ceilingsDeciding), and under
imageStripHysteresisRatio, an image whose trim the ceiling's limit alone
brought on. That attribution carried most of this change's code and tests,
and each review round found something in it. A rent pass from the least
change weighed it against what the operator needs: that a setting written
as no limit, or above a ceiling, now has one.

The line is now said once per ceiling per instance, at the first render a
select returns where images are shown as placeholders and a limit that
ceiling sets was over: under the limits, at an image the render replaces,
and under the rule, where it brought a trim on anywhere in the rule's
replay of the history, which records that as it runs. It can come where the
depth limit or a limit the host set is stripping too, and it still says
only what is true: the ceiling holds the setting, and some images are now
placeholders. A reopened instance says it at its first compile when the
history has passed a ceiling, as before.

ceilingsDeciding, the verdicts' depth flag and place, the per-image notes,
the per-trim record and ruleCeilingHits go. ceilingSetLimits, one flag per
ceiling, the line's words and the warning at initialize stay.

A window held at a ceiling strips its oldest images as new ones come, as
under any limit, but from deep in the request, so the prompt cache is
rewritten from there at every new image. The option docs and the breaking
fragment say so, and while imageStripHysteresisRatio is off the memory
strategy's line adds that cost and names the option, which strips in
batches. Windowed passthrough has no such option, so its line doesn't.

Tests: the cases where the depth limit or the host's own limit strips the
same image are turned round to say the line; the attribution tests go; new
ones pin the rule's line for a trim the host's limit or the depth also
brings on, its silence for a trim only the host's limit brings on though
the ceiling-set count is over its target, its silence at a render that
replaces no image, its reading of its own replay rather than the limits'
account, and the clause, said with the rule off and not with it on.
…ims, both ceilings at once, and texts

The memory strategy's ceiling line named imageStripHysteresisRatio as the
remedy for a window held at a ceiling even under a count ceiling of 1,
where the rule keeps the newest image at every trim and so can't space
them. The clause is now said for the count ceiling only above 1.

Tests: both ceilings' limits over at the same image, under the limits for
each strategy and under the rule, so each kind is noted and reported at a
shared strip or trim; a history with a ceiling's trim and then a trim only
the host's byte limit brings on, compiled once, so the replay's record of
the first trim stands; and the clause left out under a count ceiling of 1.

Texts: the comments say a ceiling-set limit "is over" where they said an
image "passes" it or a ceiling "decided" it; the strip's comment says the
rule notes where its set replaces an image, so an image replaced for want
of a verdict notes nothing; windowed passthrough's reportCeilings doc and
ceilingBoundLine's doc say when the line comes, the rule's reading
included; and renderAsNewest's interface doc says tool blocks come back
with what sending prunes from them, as the memory strategy's own doc
already does.
…ge-ceilings

fix(images): image ceilings for the live window and compression prompts
One conflict, the import block of src/strategies/windowed-passthrough.ts,
where each side added an import: both are kept.

The windowed select's end merged without a conflict and keeps both
changes: the system-change entries before the window that this pull
request keeps raw, and the ceiling report main adds after stripping
images. Entries of system changes carry no images, so the ceilings and
the stripping see the same images as on main.

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

feat: carry membrane's system changes in place under every strategy; firstChanged on compile results
…efusal and calibration sample

A host couldn't see what its context managers do as it happens: a refused
compile reached it only as agent-framework's classification, a refusal's
stage only as message text, and the library's own compression refusals and
calibration samples only on stderr and in the compression log. A host's
telemetry needs these facts; the library tells an observer the host gives
it, on its own terms, and learns no telemetry schema.

- ContextManagerConfig.observer (ContextObserver, src/observer.ts), with
  three optional callbacks, handed to the strategy in StrategyContext.observer.
  isolateObserver reads each callback once, calls it synchronously, and keeps
  what it throws, or a promise or thenable it returns that rejects, out of the
  work: what it returns is adopted through Promise.resolve and given its
  handler through the intrinsic then. The manager logs a callback's first
  failure once through its namespaced diagnostics and drops the rest.
- compile(record), once per compile(): strategy, dryRun, startedAt on the
  wall clock (Date.now(), so an observer adds no read of the injectable
  clock), the whole compile's durationMs, outcome ('compiled', 'refused' for
  an error named in COMPILE_REFUSALS, or 'failed'), the thrown error itself
  and a compiled compile's renderStats. compile() awaits nothing, so its body
  moves unchanged into a synchronous compileNow(), which compile() times; with
  no compile callback it returns compileNow()'s result directly.
  isCompileRefusal reads a thrown value's name without ever throwing.
- OverBudgetError.stageId, from OVER_BUDGET_STAGES. The library's sites pass
  a new stageId option typed as their ids; the constructor's stage option is
  exactly 0.21.0's, words that open the message as given. Words alone read
  'strategy-defined', a stageId beside them names the stage, neither is
  'picker-exhausted', and an id counts only as the words map's own key, as a
  string. Every call 0.21.0 accepted builds the same message byte for byte.
  AutobiographicalStrategy's seven sites pass the id and its words through
  overBudgetStage(id), so an override of the protected overBudgetError hook
  is given stage as before.
- compression(record), for the refusals the library makes itself, by a reason
  from COMPRESSION_REFUSALS: 'admission-rejected' and 'topology-violation'
  before any call; 'empty', 'incomplete' and 'malformed' of a response; the
  split rung's caps, 'chunk-call-cap' and 'window-call-cap'. Each is reported
  where it's decided: the L1 ladder's record of a rung's answer (a rung reused
  from saved progress isn't reported again), its admission refusals (decided,
  and reported, by each ladder run that reaches them), the merge gate and its
  check for an answer that strips to nothing, the crossed-merge refusal, each
  split piece's empty or incomplete answer, and a cap's trip, kept in a
  variable typed as the two cap reasons. ownCompressionRefusal decides which
  outcomes are the library's own, leaving a provider's refusal or error to
  membrane, and fails to compile until a new outcome is decided.
- calibration(record), once per sample reportRealInputTokens takes: outcome
  from CALIBRATION_SAMPLE_OUTCOMES, ratio, and the multiplier after.
- README section "Observing a manager's work"; changelog.d added fragment.
- test/context-observer.test.ts pins each record, each reason at each site
  and each piece of isolation, and checks under each of the four strategies
  that a compile's result is the same with no observer, a recording one and
  one that throws or rejects; test/overbudget-diagnostics.test.ts and
  test/system-changes.test.ts check each site's stageId beside the message
  they pin, and the latter reaches passthrough's refusal of its system
  changes and the newest message, which no test did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The L1 overlap guard's refusal is reported as 'overlap-guard', a new reason
  in COMPRESSION_REFUSALS, once per chunk as its log line is: a refusal the
  library makes on its own judgment, which a host watching for store damage
  wants to see.
- Text: a provider's refusal or error on a compression call is the
  provider's decision, which context-manager doesn't report (the comments
  no longer say membrane reports it). The README, the option's doc, the
  ContextObserver doc and the changelog name the one case isolation can't
  cover: a returned native promise whose constructor or species getter
  throws, which no JavaScript can attach a handler to. isReady() and
  compile() no longer say compile() waits for background work.
- isolateObserver's failure sink is required; its silent default was unused.
- Tests: an observed preview leaves the strategy as an unobserved one does,
  so the options reach the work; a compile that throws is timed whole; each
  record is taken before the caller awaits; the throwing-log test checks the
  log was asked; the overlap guard's report; and a shared check
  (test/fixtures/over-budget-hook.ts) that every refusal through the
  protected overBudgetError hook gives it the stage's words, or none for the
  picker's own refusal, beside each stageId the existing tests check,
  calibration-band's picker refusal included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erlap guard's report documented as it runs

- A native promise an observer callback returns, a subclass's included, now
  takes the rejection handler on itself through the intrinsic then, so a
  then of its own can't skip it; Promise.resolve adopts only what isn't
  one. The docs name the limit as a kind: a promise no JavaScript can give
  a handler (one whose constructor or species can't be used, or a proxy
  around one) is logged, and its rejection goes unhandled, which ends a
  Node process by default.
- The overlap guard's doc: KnowledgeStrategy leaves a partial overlap in
  ordinary chunking, not only damaged bookkeeping; a chunk with a record
  isn't queued again while refused, one without is refused again each time
  it's queued; it's reported once per chunk until the strategy loads its
  state again, which also happens when it takes up what another instance
  sharing the store wrote or sets a branch's state up again.
- ReadinessState and checkReadiness no longer say compile() waits for
  background work; _overlapBlocked's comment says what it holds.
- Tests: a rejecting subclass promise with a then of its own; a proxy
  around a promise, logged, the compile unchanged; the overlap guard reports
  again after a state load and not for a chunk an L1 covers whole; the
  hook check pins each stage's words independently; the provider-refusal
  test names no longer credit membrane.

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

A returned promise no JavaScript can give a handler keeps its own
rejection unhandled; the public docs (ContextObserver, the README, the
changelog) no longer say what it throws on the way is logged, since for
one shape nothing is logged: a promise whose species can't be used and
whose own then is undefined is adopted as a plain value. The code comment names
that shape. Comments and docs only.

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

feat(observer): tell a host's observer of each compile, compression refusal and calibration sample
…ds at the new outcomes

main brings anima-research#335, whose observer reports each
calibration sample from the out-of-band path as it was before this branch.
The conflict is that path: this branch's structure stands, and the observer
is told at each of its exits:

- a rejected out-of-band sample, a declared one counting toward a run
  included: 'out-of-band', the multiplier unchanged;
- the first declared sample on a multiplier no declared sample has marked,
  held for the next declared sample within the band: 'deferred', a new
  outcome in CALIBRATION_SAMPLE_OUTCOMES, the multiplier unchanged;
- every outright replacement, a consistent run's last sample and the mean
  of two agreeing declared samples as well as a provisional multiplier's:
  'replaced', or, when the result is within the dead band of the
  multiplier in force, 'held' ('confirmed' for a provisional multiplier),
  as any sample there is; an EMA step: 'stepped'.

The observer's calibration tests open a store holding a learned record,
since a store with none is now provisional
(anima-research#328). New tests cover a declared and an
undeclared run, a run at the clamp on a learned and on a provisional
multiplier, both ends of a deferred sample, a pair within the dead band, an
undeclared move taking the mark back, a rejected sample between a pair, the
ratio each record carries, and a store with no record, declared or not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t a declared caller paid

- RenderStats.calibration: the 1 loaded beside a record from a newer
  pricing epoch, or a record the store can't read, is neither learned nor
  provisional; a newer epoch's record with no usable multiplier isn't the
  no-record case.
- The fragment for anima-research#328: a learned 1 took
  nine applied changes per undeclared caller, and two samples, the first
  held, for a declared one.
- The load's catch says why an unreadable record isn't the no-record case,
  rather than pointing at that branch.

Comments, docs and the changelog only.

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

Copy link
Copy Markdown
Author

Closing this: it brought anima-research#335's observer into anima-research#324's branch, and anima-research#340 has since reverted that observer on main (merged as 2786b608). The head of anima-research#324, d2bd6fb7, now merges with main without conflicts, and so does #2. The one commit here that corrected #2's own texts, 57643cbe, is now on that pull request as e0dea9ac.


Written by Esther-2584 (Claude Opus 5.5).

— Lottie-2661

@nissa-seru nissa-seru closed this Oct 26, 2026
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