Skip to content

fix(cap): the speculative cap holds L1 production only while an L1 merge is queued - #243

Merged
nissa-seru merged 1 commit into
anima-research:mainfrom
nissa-seru:fix/240-speculative-cap-relievable
Oct 19, 2026
Merged

nissa-seru merged 1 commit into
anima-research:mainfrom
nissa-seru:fix/240-speculative-cap-relievable

Conversation

@nissa-seru

Copy link
Copy Markdown
Contributor

TL;DR: past maxSpeculativeL1s, compressing chunks into L1s now waits only while an L1→L2 merge is queued, with hierarchical on, to bring the count of unmerged L1s back down. On main it waits whenever the count is past the cap, so where no merge can come it waits forever: with a cap below mergeThreshold - 1, with unmerged L1s that can't merge filling the cap (as after a merge goes to the merge quarantine), and under hierarchical: false. The option's documentation also described another rule, and now says this one.

Fixes #240.

Problem

On 40987192, three places read the cap as a stop on L1 production: tick takes no chunk while isAtSpeculativeCap() (src/strategies/autobiographical.ts:5265), the background drain stops (:5182), and the over-budget work count leaves the queued chunks out (:9516). Each assumes a merge will bring the count of unmerged L1s back under the cap. Three states break that:

  • A cap below mergeThreshold - 1. No merge forms below mergeThreshold unmerged L1s (contiguousMergeCandidates, :8398), and production stops one L1 past the cap, short of that.
  • Unmerged L1s that can't merge. They count toward the cap and never leave it: a summary whose sources resolve to no message, one in the wide-span quarantine, a lone interior one, and the sources of a merge in the merge quarantine. That last case stops every later L1 merge too: contiguousMergeCandidates offers the oldest run that qualifies, its first mergeThreshold members (:8469), so while that run is the quarantined one it offers the same set each time, and enqueueMerge refuses it (:4385).
  • hierarchical: false. tick runs no merge.

Each stops for good, since no tick, message or compile changes the count. On main, 40 short messages (8 chunks), a merge threshold of 6, a provider that always answers and 50 ticks gave:

Case Chunks compressed
No cap 8 of 8
Cap 2 3 of 8
Cap 4 5 of 8
Cap 5 8 of 8
Cap 5, with two L1s whose sources resolve to no message 4 of 8
Cap 7, with the same two 8 of 8
Cap 5, hierarchical: false 6 of 8

On main with the merge quarantine, 80 short messages (16 chunks), a threshold of 3, a cap of 4, mergeAttemptLimit: 1, and a provider that refuses the first merge and answers everything else, 80 ticks compressed 5 of 16 chunks, on both merge checks (speculativeProduction off and on).

The option's documentation (src/types/strategy.ts:1360 on main) said the cap counts queued chunks as well as unmerged L1s, and holds back only onNewMessage's auto-tick, leaving a manual tick() or a compile free to compress. The code counts produced, unmerged L1s, and tick itself honours it.

Changes

  • speculativeCapHolds() is the one rule for whether the cap holds L1 production now. It holds past the cap, while an L1→L2 merge is queued, with hierarchical on. tick's L1 step and the over-budget work count read it.
  • The background drain no longer checks the cap. It reached that check only when no merge was queued or hierarchical was off, and the cap holds nothing in either case. With a merge queued, the drain ticks, and tick's L1 step comes first and reads the cap.
  • The option's documentation, the comments on isAtSpeculativeCap, tick, the drain and the work count, the picker's doc of clearedBy, the design doc's line on the cap and the cap tests' header say what holds and when. A changelog fragment says it for hosts.

What a host will notice

  • A host whose compression stopped at the cap compresses again after the upgrade. It compresses its queued chunks as it would have when they formed, and then merges where merges run.
  • With a cap below mergeThreshold - 1, unmerged L1s rise to the threshold, where a merge forms, and production waits for that merge. On main they stopped one past the cap, and so did compression.
  • Under hierarchical: false, the cap holds nothing.
  • Behind a quarantined merge, compression goes on, but the L1s still don't merge until the quarantine is cleared. In the run above, this change compressed 16 of 16 chunks and wrote no L2.
  • A subclass whose shouldCompressPreflight says no sees the drain compress the queued chunks past the cap when only a merge above L1 is queued, as it already did below the cap. By reading, the drain passes over the preflight for as long as any merge is queued with hierarchical on, and tick takes one chunk a call before it reaches a merge, so this goes on until the compression queue empties, or until an L1 merge is queued and the cap holds again. Outside its tests, nothing in this repository or in connectome-host overrides the preflight.
  • With hierarchical on, a cap of at least mergeThreshold - 1 and no L1s that can't merge, nothing changes. By reading, past the cap a merge is queued, since checkMergeThreshold runs after each L1 tick writes and after each merge, and queues one once there are mergeThreshold unmerged L1s in a run that can merge. Production waits for it as before. One exception by reading: an exact L1 tick adopts from the store runs no merge check, so a run it completes waits for the next L1 tick writes, which the cap no longer blocks there. The documented connectome-host setting, a cap of 36 with a threshold of 6, meets the first two conditions.

What each part is for

  • speculativeCapHolds is one answer to "does the cap hold now", read where the cap used to be read. Its conditions:
    • Only an L1→L2 merge holds. It's the only merge that brings the L1 count down; a queued merge above L1 doesn't (a test pins it).
    • hierarchical must be on. Under hierarchical: false, checkMergeThreshold still queues a merge that tick never runs (the hierarchical: false case above ended with one queued), so without the condition the cap would hold forever there. It reads the flag as tick's merge step does.
    • A queued L1 merge holds however long it waits. A test pins it behind another merge at the head. By reading, it holds the same way while a compression hold over its sources blocks it, or while its calls fail and retry, as on main, since the rule looks only at what the queue holds.
  • Dropping the drain's cap check. Where the drain read the cap, no merge was queued or hierarchical was off, so the new rule would never hold there.
  • Not taken:
    • Rejecting or clamping the cap at construction. It covers only the low-cap case, not L1s that can't merge or hierarchical: false, and a rejection would turn a host whose compression stopped into one that doesn't start after the upgrade.
    • Merging the newest run early past the cap. It changes the summaries' fan-out, and does nothing for hierarchical: false.
    • Holding only for an L1 merge tick can run now. By reading:
      • A compression hold already stops compression of every message from its boundary on (holdBlockedIds), so the chunks behind a merge it blocks wait anyway.
      • How long a failing merge stays queued is up to the merges' error handling. tick retries one that fails with a rate limit, a network error or a timeout, and moves one that's refused, or keeps failing with another provider error, into the merge quarantine. A merge that never succeeds stops every merge behind it, so bounding it belongs there, not to the cap.

Out of scope

Other open pull requests

Tests

In test/speculation-cap.test.ts:

  • new: each of the three cases compresses every chunk, under both merge checks (speculativeProduction off and on), and merges once the newest run reaches the threshold where merges run: a cap of 2; a cap of 5 with two L1s whose sources resolve to no message; and a cap of 5 under hierarchical: false;
  • new: the auto-tick drain does the same past the cap, with the two such L1s;
  • turned round: past a cap of 2 with three such L1s and no merge queued, the auto-tick fires. It used to assert that it didn't;
  • new: past the cap, tick takes no chunk while an L1 merge is queued, and takes one once the merge has run, also with the L1 merge queued behind a merge above L1;
  • new: under the cap, a queued L1 merge holds nothing;
  • new: past the cap, a queued merge above L1 holds no L1 production.

In test/overbudget-diagnostics.test.ts:

  • the queued chunks past the cap count as compression work with no merge queued (turned round from "nothing"), the queued merge counts when one is queued, and nothing counts while that merge is blocked;
  • the test of the work in flight queues an L1 merge that covers no message, so the cap holds and the chunk in flight is again all that can count.

Checks

On Linux:

  • Red-first, with Node 24.19.0: with main's source and these two test files, 10 tests fail. They are the six case runs, the auto-tick drain case, the turned-round auto-tick test, the merge above L1, and the turned-round count. The other new and changed tests pass on main as well, since there the cap held whenever the count was past it, and never under it.

  • Removals, with Node 24.19.0: each of nine pieces, taken out alone, fails at least one test:

    • tick's L1 step reading the bare cap (8);
    • the work count reading it (1);
    • the drain's cap check put back (2);
    • the rule holding without a queued merge (9);
    • the rule never holding (4);
    • the rule ignoring hierarchical (2);
    • any queued merge holding rather than an L1 merge (1);
    • the rule holding under the cap too (4);
    • only a merge at the head holding (1).

    Taking out the count of the chunk in flight, which is main's, fails the test of the work in flight.

  • The whole suite, with Node 24.22.0: 1,949 of 1,949.

Not verified

  • A live host whose compression stopped at the cap. The cases above are a test store's.

  • Changelog fragment added: changelog.d/240-speculative-cap-relievable.fixed.md.

— Beth-2389 (Claude Opus 5.5). Anarchid reported the defect in #240. Oscar-1005 (Claude Opus 5.5) agreed the design, an internal review read the change, and Abigail-2459 (Claude Opus 5.5) read it and its fix round. 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.

…rge is queued (anima-research#240)

Past maxSpeculativeL1s, tick took no chunk to compress, the background
drain stopped, and the compression work count left the queued chunks
out, on the premise that a merge would bring the count of unmerged L1s
back down. Where no merge could, compression stopped for good, and no
tick, message or compile moved it:

- a cap below mergeThreshold - 1, since no merge forms below
  mergeThreshold unmerged L1s;
- unmerged L1s that can never merge filling the cap: a lone interior
  summary, a pruned source, the wide-span quarantine, or the sources of
  a merge in the merge quarantine, which contiguousMergeCandidates
  offers again while its run is the oldest that qualifies, so
  enqueueMerge refuses it and no later L1 merge forms;
- any cap under hierarchical: false, where tick runs no merge.

speculativeCapHolds() is now the one rule: the cap holds L1 production
only while an L1->L2 merge is queued and hierarchical is on, read as
tick's merge step reads it. tick's L1 step and the work count read it.
The drain no longer checks the cap itself. It reached that check only
with no merge queued or with hierarchical off, and the cap now holds
nothing in either case. With a merge queued, tick's L1 step comes first
and reads the cap. Production waits for a queued L1 merge however long
that merge waits, on a hold over its sources or through failing calls,
as it did on main.

The option's doc said the cap counted queued chunks and held back only
the auto-tick. It now says when the cap holds, that a cap below
mergeThreshold - 1 lets unmerged L1s rise to the threshold, and that
under hierarchical: false it holds nothing. The comments on
isAtSpeculativeCap, the drain and the work count, the clearedBy doc in
the picker, the design doc's line and the test file's header said the
old rule, and now say this one. A changelog fragment says it for hosts.

Tests: each of the three states compresses every chunk, through both
merge checks (checkMergeThreshold and the recursive one under
speculativeProduction), and merges once the newest run reaches the
threshold where merges run; the auto-tick drain does the same past the
cap; auto-tick fires past the cap with no merge queued, where the old
test asserted the stall; past the cap, tick takes no chunk while an L1
merge is queued, also behind a merge above L1 at the head, and takes
one once it has run; under the cap, a queued L1 merge holds nothing; a
queued merge above L1 holds no L1 production; and the over-budget count
names the queued chunks past the cap only when no L1 merge is queued.
The in-flight count's test queues an L1 merge that counts nothing, so
the cap holds and the work in flight is again all that can count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@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 0129fb2 into anima-research:main Oct 19, 2026
7 checks passed
nissa-seru pushed a commit to nissa-seru/context-manager that referenced this pull request Oct 19, 2026
…its as below it

anima-research#243, merged from main in the commit before this, makes the speculative
cap hold L1 production only while an L1 merge is queued to bring the count
back down. Before it, tick took no chunk past the cap, so the retry that
would fill a hole among the unmerged L1s never came there, and a waiting L1
run merged at two as a last resort so that the count could fall back under
the cap. With anima-research#243's rule the retry comes once the count is back under the
cap or no L1 merge is left queued, so the last resort only merged lopsided
before a summary on its way. It goes, and contiguousMergeCandidates treats
a waiting run past the cap as it does below it.

The comment, the changelog fragment's sentence and three tests follow. The
first test now pins that past the cap the run still waits and that, with no
L1 merge queued, the cap holds no L1 production. The other two keep their
cases, with messages that no longer name a last resort.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

maxSpeculativeL1s below mergeThreshold stalls compression for good, and its doc describes a different rule

1 participant