Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
a0efb9f
fix(images): request ceilings for images and image bytes, with the li…
Oct 19, 2026
1098418
fix(images): say once when an image first falls past a request ceiling
Oct 19, 2026
52f0a18
fix(images): the ceiling line from sent renders only, its words and m…
Oct 19, 2026
6287092
fix(images): the memory strategy's ceiling line once a select returns…
Oct 19, 2026
b28bdde
fix(images): each select's own ceiling hits, and a watermark's move k…
Oct 19, 2026
87013f3
fix(images): name the ceilings for what they bound, and say the line …
Oct 20, 2026
5e082b8
Merge main (776391df) into fix/request-image-ceilings
Oct 20, 2026
ae432a3
docs(strategy): renderAsNewest's tool pass only pairs; pruning reache…
Oct 20, 2026
ec108e8
fix(images): a ceiling decides a watermark move only where it lands i…
Oct 20, 2026
5279d2d
test(images): the watermark names the count ceiling, not the host's o…
Oct 20, 2026
043130f
fix(images): say a watermark's ceiling line only for an image the cei…
Oct 20, 2026
5aaf37d
fix(images): keep a watermark move's record until each ceiling it hol…
Oct 20, 2026
1cf163e
fix(images): a ceiling's watermark move says nothing for an image the…
Oct 20, 2026
91df717
test(images): the host's own count limit, by render time, quiets a ce…
Oct 20, 2026
706198c
docs(images): the ceiling line's ending, membrane's checks, and a loa…
Oct 21, 2026
f914a99
Merge main (17a7f6d6) into fix/request-image-ceilings
Oct 21, 2026
b54138d
Merge main (48bb87a1) into fix/request-image-ceilings
Oct 22, 2026
b41c436
docs(images): the ceilings count images as #219's limits do
Oct 22, 2026
2f9e7f3
refactor(images): windowed passthrough notes every image's deciding c…
Oct 22, 2026
bbca9e3
feat: carry membrane's system changes in place under every strategy; …
Oct 22, 2026
a15cdf7
fix: refuse a system change between a tool call and its result; compa…
Oct 22, 2026
3b1b9e9
Merge main into feat/system-change-entries
Oct 22, 2026
681525f
Merge main (6846343c, #311) into feat/system-change-entries
Oct 22, 2026
c9cc061
Merge main (570303ec, context-manager 0.19.0) into feat/system-change…
Oct 22, 2026
c2ecdac
chore(deps): membrane floor ^0.7.1, not ^0.7.0
Oct 23, 2026
2ec900b
Merge main (23f6cad2, context-manager 0.20.0) into feat/system-change…
Oct 23, 2026
4e8ee43
changelog: drop the membrane-floor entry; main requires ^0.7.1 since …
Oct 23, 2026
b111648
Merge main (04bb8834, context-manager#312) into feat/system-change-en…
Oct 23, 2026
01d830c
Merge main (fa6e0d6a) into fix/request-image-ceilings
Oct 23, 2026
acc1feb
docs(images): the ceilings' docs and fragments follow the rule's credit
Oct 23, 2026
585df8d
test(images): pin four pieces of the ceiling line no test caught
Oct 23, 2026
8c40a65
docs(images): review round: the rule's credit names its third conditi…
Oct 23, 2026
b7e7312
test(images): review round: four cases of the ceiling line the round'…
Oct 23, 2026
f5c4216
fix(compression): a request for history gives it only what came befor…
Oct 23, 2026
895c368
fix(compression): read heads once per count of queued work; narrow cl…
Oct 23, 2026
bcf8bcd
test(compression): drop a reviewer's name from two comments (#317)
Oct 23, 2026
178a774
Merge pull request #322 from nissa-seru/cedric2315/head-as-of
nissa-seru Oct 23, 2026
86984f6
fix(calibration): price message-position injections into the sample; …
Oct 23, 2026
b8a617a
docs(changelog): #322's recall change under Changed (#317)
Oct 23, 2026
ca9b9d0
fix(pins): a pin renders whole messages in the selects; a compile tha…
Oct 23, 2026
7049ff3
docs: the recall change's exceptions, and a comment #322 left stale (…
Oct 23, 2026
79b2d17
Merge pull request #326 from nissa-seru/cedric2315/recall-changed
nissa-seru Oct 23, 2026
7df5bb6
fix(calibration): a store with no record starts from a provisional 1 …
Oct 23, 2026
e93803d
docs(calibration): give a consistent run's actual reason for replacin…
Oct 23, 2026
dd98a98
fix(pins): narrowings end when the strategy loads the store again; on…
Oct 23, 2026
0215b1b
fix(calibration): answer our internal review of the round
Oct 23, 2026
7099a7b
docs(calibration): say what the no-record, newer-epoch and drop paths…
Oct 23, 2026
eb80967
fix(pins): a shard locked at a fold counts as freed; every put-back r…
Oct 23, 2026
666de27
fix(pins): a part locked with lockChunk keeps its lock under a mark o…
Oct 23, 2026
15ccf84
chore(release): 0.20.1
Oct 23, 2026
d6bb4c5
fix(pins): name kv-stable's summary hold over a lock; tests for both …
Oct 23, 2026
f1d69e7
docs(pins): kv-stable's summary hold is a pin with level's, not maxLe…
Oct 23, 2026
8db49e6
Merge pull request #327 from nissa-seru/fix/whole-body-pins
nissa-seru Oct 23, 2026
4efbd35
Merge main (8db49e6b, context-manager#327) into feat/system-change-en…
Oct 23, 2026
e06edf0
feat(thinking-binding): leave out thinking a binding model can no lon…
Oct 23, 2026
edef413
fix(pins): every reader reads a pin over a long message whole, and th…
Oct 23, 2026
5f5ed6a
fix(thinking-binding): digest a noted row as the compile sends it; pr…
Oct 24, 2026
1c0f911
fix(pins): a remedy short enough to survive a quoted cut, every raw p…
Oct 24, 2026
6f3b851
fix(pins): the plan's middle excludes its tail under a disabled head,…
Oct 24, 2026
c22f251
docs, test(thinking-binding): the record's noted digests per part; re…
Oct 24, 2026
6dd2a82
fix(thinking-binding): a compile without binds touches no binding sta…
Oct 24, 2026
30a7917
docs, test(thinking-binding): the record is a binding compile's; a no…
Oct 24, 2026
c811936
Merge pull request #330 from nissa-seru/fix/one-pin-reading
nissa-seru Oct 24, 2026
9f22950
Merge main (c8119361, context-manager#330) into feat/system-change-en…
Oct 24, 2026
58a6884
fix(system-changes): the refusal's raw-pin tokens leave out messages …
Oct 24, 2026
b1a3dff
docs(changelog): note that marks larger than the summarizer's budget …
Oct 24, 2026
0060b14
docs, test(system-changes): the refusal's raw-pin doc names what it l…
Oct 24, 2026
6feb9ea
docs(changelog): narrow the note on marks the summarizer's budget can…
Oct 24, 2026
ff4c035
Merge pull request #333 from nissa-seru/fix/332-changelog-note
nissa-seru Oct 24, 2026
656b2e9
chore(release): 0.21.0
Oct 24, 2026
4fbe6c9
Merge pull request #331 from nissa-seru/feat/thinking-binding
nissa-seru Oct 24, 2026
03a5f77
Merge main (4fbe6c94) into fix/request-image-ceilings
Oct 24, 2026
b089906
docs(changelog): the siblings' ranges after 0.21.0
Oct 24, 2026
64f9016
Merge main 4fbe6c94 (thinking binding) into the system-change entries
Oct 24, 2026
9aa00fb
docs(changelog): connectome-host 0.13.1's range
Oct 24, 2026
0b80867
fix(images): the rule's ceiling line is credited per trim (R2-1)
Oct 24, 2026
b6bd677
fix(compression): the kept counts are filled with no cap set (R2-6)
Oct 24, 2026
40c90e4
docs(images): the shed note names what membrane 0.7.1 sheds, and the …
Oct 24, 2026
186641b
refactor(images): the ceiling line says a ceiling's limit was over, w…
Oct 24, 2026
70bdf17
fix(images): review round: the clause where the option can't space tr…
Oct 24, 2026
f387896
Merge pull request #268 from nissa-seru/fix/request-image-ceilings
nissa-seru Oct 24, 2026
df3786b
Merge main f3878966 (image ceilings) into the system-change entries
Oct 24, 2026
7f34a7d
Merge pull request #298 from nissa-seru/feat/system-change-entries
nissa-seru Oct 24, 2026
9a14727
feat(observer): tell a host's observer of each compile, compression r…
Oct 24, 2026
8796937
chore(release): 0.22.0
Oct 24, 2026
292f724
fix(observer): answer the automated review of 9a14727
Oct 24, 2026
2c11a7c
fix(observer): native promises take the handler on themselves; the ov…
Oct 24, 2026
55033a6
docs(observer): the isolation limit says only what holds for every su…
Oct 25, 2026
50c28f7
Merge pull request #335 from nissa-seru/feat/context-observer
nissa-seru Oct 25, 2026
066a6c8
Merge main into the calibration fix: the observer's calibration recor…
Oct 25, 2026
57643cb
docs(calibration): the exceptions to learned and provisional, and wha…
Oct 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,18 +84,24 @@ How it works, with each option's default in parentheses:
- With `adaptiveResolution: true` (false), each compile chooses how deeply to fold each part of history to fit the budget. `foldingStrategy` names the solver: `flat-profile` (the default), `oldest-first`, `kv-stable` or `kv-unified`. `kv-unified` needs an explicit `kvUnified` policy.

With a strategy that supports them, as this one does, the manager also offers:
- `pinRange` and `markDocument` keep messages at their place in the conversation, raw by default. A pin that sets `level` or `maxLevel` holds its messages at that level, or no deeper than it, only with `adaptiveResolution` and the `kv-stable` or `kv-unified` solver. Under the other solvers, and without adaptive resolution, the pin keeps them raw. `listPins` and `unpin` manage the pins.
- `pinRange` and `markDocument` keep messages at their place in the conversation, raw by default. A pin that sets `level` or `maxLevel` holds its messages at that level, or no deeper than it, only with `adaptiveResolution` and the `kv-stable` or `kv-unified` solver. Under the other solvers, and without adaptive resolution, the pin keeps them raw. Under adaptive resolution, a pin or mark on any part of a long message stored in parts covers the whole message, in what the agent is sent, in compression and in the summarizer's prompt alike, less a part locked with `lockChunk` outside the span its ids name. As with any pin, a chunk recorded before it keeps its messages, so compression can still summarize those parts while the agent is sent them raw. Under `kv-stable`, a pin with `level` (not `maxLevel`) holds whole each summary it reaches, so a locked part that shares a summary with a part the pin covers renders at the level the pin holds that summary at. A compile whose budget can't hold the raw pins beside the rest refuses with `OverBudgetError`; where the adaptive plan's middle is over the budget, its message and `diagnostics.rawPins` name each pin keeping raw tokens in that middle. `listPins` and `unpin` manage the pins.
- `holdCompression` keeps messages out of compression until `releaseCompression`, or until an optional `timeoutMs` passes, for content you'll still edit. Holds live in memory, so a reopened manager starts with none, and a hold that's never released stalls compression of everything after it.
- `searchSummaries`, `getSummary` and `getSummariesInRange` read the summaries, for memory tools an agent can call.
- `setToolDefinitions` and `setSystemPrompt` give the summarizer's calls the agent's own tools and system prompt. A chunk whose messages hold tool calls or results waits to be compressed until tool definitions are set.
- `getRenderStats` reports how much of the last compile was raw and how much was summaries, with counts for levels 1 to 3. It's `null` until a live compile has run since the strategy loaded its branch, which opening the store, `setStrategy()` and a branch switch each do; a dry compile or preview leaves it as it was. Each compile's result carries its own as `renderStats`, so a dry compile (`dryRun`) shows the composition of the request it built without sending it.

For a model that binds each signed thinking block to the request it was minted in, pass `{ thinkingBinding: { binds: true, dropBlock, participant }, identity }` as compile's third argument, with `participant` the name the model's own replies are stored under and `identity` digests of the request's system text, tools, model and the rest of its head. The compile then leaves out the thinking such a model can no longer take, against a per-branch record of the last request the branch sent, and says what it newly left out in `thinkingStrip`. Once the branch has a record, a reply keeps its thinking at the compile after it's stored only if the host has noted it with `noteRequestSent(rows, identity, compileId)`, naming the `provenance.compileId` of the compile its stream was built from: as it sends each later round of a stream, the ids of the stored messages the request carried after the compiled ones, every round's so far, in order, and once the stream has ended, those and its last reply's, with `{ unstored: true }` for a row it can't vouch was carried or returned as stored. A host that doesn't call it loses each reply's thinking at the compile after it's stored, and is never refused for it. While one of a branch's requests is still streaming, compile for that branch with `binds` only with `dryRun`: a committing compile with `binds` makes that stream's later notes vouch for nothing (see `SelectOptions.thinkingBinding` in `src/types/strategy.ts`).

Every option is documented on `AutobiographicalConfig` in `src/types/strategy.ts`.

## Branches

Chronicle keeps the store's history, so a conversation can branch. `branchAt(messageId)` creates a branch as of a message, and `switchBranch(name)` moves to it. `fork()` creates a branch at the current head and switches to it. The strategy's state, summaries included, follows the branch.

## Observing a manager's work

Pass `observer` to `ContextManager.open` to be told, as it happens, of each compile (`compile`: compiled, refused or failed, its whole duration, the thrown error, and the compile's render stats), of each refusal context-manager itself makes of compression (`compression`: by a reason from `COMPRESSION_REFUSALS`), and of each calibration sample (`calibration`: by an outcome from `CALIBRATION_SAMPLE_OUTCOMES`). The manager hands the observer to its strategy too. Each callback is optional and called synchronously. What it throws or rejects never reaches the work: the manager logs a callback's first failure once through its diagnostics, under its namespace, and drops the rest, so a compile's result is the same with an observer as without one. Outside that is a returned promise no JavaScript can give a handler, such as one whose `constructor` or species can't be used, or a proxy around one: its rejection goes unhandled, which ends a Node process by default. A record's error and render stats are the objects the caller gets, so treat them as read-only. A refused compile's `OverBudgetError` names its stage in `stageId`, from `OVER_BUDGET_STAGES`. A record can hold text from the store, such as a refusal's message, which names pins, so choose what you keep or send from it.

## Inspecting and repairing stores

Among other tools, `scripts/` holds ones for inspecting and repairing stores. For example, `inspect-store.ts` writes out a store's messages, summary tree and resolutions, `audit-topology.ts` checks that each summary covers a contiguous run of messages, and `repair-topology.ts` plans a repair for one that doesn't, which it applies with `--apply`. Opening a store takes its lock and can write to it, so run them on a stopped agent's store or on a copy. `scripts/README.md` shows how to compile a script and describes several of them.
Expand Down
1 change: 0 additions & 1 deletion changelog.d/175-load-repair-report.added.md

This file was deleted.

40 changes: 29 additions & 11 deletions changelog.d/323-calibration-request-overhead.fixed.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,34 @@
estimate doesn't, so a tool-heavy agent's multiplier learned its schemas
as a per-token rate and folded history the budget had room for, or sat at
1.00 rejecting every sample as out of band. A caller that passes nothing
gets the old comparison.
gets the old comparison, plus the compile's message-position injections
(below).
- With the overhead declared, three out-of-band calibration samples in a row
whose implied multipliers agree within 10% are learned, outright, at the
clamp edge, instead of being rejected for good. At 1.00 the ratio and the
implied multiplier are the same number, so a store whose true ratio was
past 1.8, where every pricing-epoch discard restarts, could never learn.
Undeclared samples are rejected as before, and the rejection line says the
caller may be leaving out the system prompt and tools it sends.
whose implied multipliers agree within 10% of the first are learned,
outright, at the clamp edge, instead of being rejected for good. At 1.00
the ratio and the implied multiplier are the same number, so a store whose
true ratio was past 1.8, where every pricing-epoch discard restarts, could
never learn. Undeclared samples are rejected as before, and the rejection
line says the caller may be leaving out the system prompt and tools it
sends.
- The sample also counts the compile's message-position injections
(`beforeUser` and `afterUser`), which the request carries and the window
estimate doesn't: `ContextManager.compile` passes their content to the
select as `SelectOptions.messageInjections`, and the select prices it with
the window. Left out, an agent whose injections are large beside its window
learned them as a per-token rate, or, with the overhead declared, at the
clamp.
- **Host operators:** a multiplier learned before the caller declared its
overhead holds that overhead, and steps back down over about ten compiles
once samples declare it. To relearn in one sample, seed it as provisional
on the stopped store: `setStateJson('<ns>/autobio:calibration',
{ multiplier, at, pricing, provisional: true })`.
overhead holds that overhead. Inside the calibration band, the first two
samples that declare it replace the multiplier with their mean when they
agree within 10%, and the EMA steps as before when they don't; either way
the record then carries `declaredOverhead: true`, so later declared
samples step by the EMA. (Out of the band, the three-sample run above
applies.) The first of the two is held in memory, so a restart between
them costs one more sample. An undeclared sample that moves the multiplier
writes the record without the flag. A store whose multiplier, above 1,
puts every compile over budget gets no sample to move it, as before: to
unwedge it, seed the record as provisional on the stopped store, and the
refused compile drops it to 1: `setStateJson('<ns>/autobio:calibration',
{ multiplier, at, pricing: <current epoch>, provisional: true })`.
`RequestOverhead` is exported from the package.
10 changes: 10 additions & 0 deletions changelog.d/328-fresh-calibration-provisional.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
- A store with no calibration record, or a record with no finite
multiplier, starts from a provisional 1, as a discard's restart does
(#328). Its first sample inside the calibration band sets the multiplier,
declared or not: one sample, without the two-sample hold. Learned, a 1
stepped a fifth of the way per undeclared sample (nine applied changes,
each a reprice of every message and summary, to reach a true 1.4), and
took two declared samples, the first held. Until that
sample, render stats report `calibration.provisional: true`. A record
from a newer pricing epoch, or a read of the record that throws, still
loads 1 as before, not provisional.
1 change: 0 additions & 1 deletion changelog.d/compile-render-stats.added.md

This file was deleted.

38 changes: 38 additions & 0 deletions changelog.d/context-observer.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
- `ContextManagerConfig.observer` (`ContextObserver`): a host is told, as each
happens, of every compile, of each refusal context-manager itself makes of
compression, and of each calibration sample. The manager hands the observer
to its strategy in `StrategyContext.observer`. Each callback is optional and
called synchronously. What it throws or rejects never reaches the work: the
manager logs a callback's first failure once through its diagnostics and
drops the rest, so a compile's result is the same with an observer as
without one. Outside that is a returned promise no JavaScript can give a
handler, such as one whose `constructor` or species can't be used, or a
proxy around one: its rejection goes unhandled, which ends a Node process
by default. A record's
error and render stats are the objects the caller gets, so an observer
treats them as read-only.
- `compile(record)`, once per `compile()`: the strategy, `dryRun`,
`startedAt` on the wall clock (`Date.now()`), the whole compile's
`durationMs` on the monotonic clock, `outcome` (`'compiled'`, `'refused'`
for one of `COMPILE_REFUSALS`, or `'failed'`), the thrown `error` itself,
and a compiled compile's `renderStats`, the object its result holds.
- `compression(record)`: a `reason` from `COMPRESSION_REFUSALS`
(`'admission-rejected'`, `'topology-violation'`, `'empty'`, `'incomplete'`,
`'malformed'`, the split rung's caps, `'chunk-call-cap'` and
`'window-call-cap'`, and `'overlap-guard'`, a chunk refused because an L1
already covers part of it). A provider's refusal or error on a compression
call isn't one of these.
- `calibration(record)`, once per sample `reportRealInputTokens` takes: an
`outcome` from `CALIBRATION_SAMPLE_OUTCOMES` (`'out-of-band'`,
`'deferred'`, `'held'`, `'confirmed'`, `'replaced'`, `'stepped'`), the
`ratio`, and the `multiplier` after.
- `OverBudgetError.stageId` names which stage refused, from the exported
`OVER_BUDGET_STAGES`, so a host needn't parse the message. The library's own
strategies give it through the constructor's new `stageId` option, whose
words open the message when no `stage` is given. The constructor's `stage`
is unchanged: a strategy's own words, which open the message exactly as
before. Given words alone, `stageId` reads `'strategy-defined'`, a new id;
given a `stageId` beside them, it reads that id; given neither, it's
`'picker-exhausted'`. Every message reads as before, and
`AutobiographicalStrategy`'s protected `overBudgetError` is still given each
stage's words as `stage`.
8 changes: 8 additions & 0 deletions docs/adaptive-resolution-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,14 @@ This case should be rare in practice with unbounded L_n: the picker can almost a
> pressure, but any such eviction that would silently lose content instead
> **refuses the turn** (`UncoveredDropError`) — pressure never ships silent
> loss. The philosophy stands: surface the overage, let the host decide.
>
> *(Raw pins)* The refusal of a plan whose middle is over the budget names
> each pin that keeps raw tokens there (`OverBudgetDiagnostics.rawPins`), with
> the tokens it keeps. Under adaptive resolution a pin on part of a message
> stored in parts covers the whole message, so a mark on a long message can
> be what doesn't fit: whoever manages the pins can unpin it, which lets
> compression fold it, or whoever sets the budget can raise it, where the
> model's window allows.

### 3.11 Recent-window slide

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@animalabs/context-manager",
"version": "0.20.0",
"version": "0.22.0",
"description": "Context management layer for LLM-based agents using Chronicle and Membrane",
"type": "module",
"main": "dist/src/index.js",
Expand Down
1 change: 1 addition & 0 deletions src/adaptive/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export {
accountFrontier,
type OverBudgetClearedBy,
type OverBudgetDiagnostics,
type OverBudgetRawPin,
type PickerInputs,
type PickerResult,
type PickerChunk,
Expand Down
Loading
Loading