Skip to content

fix(kv-unified): own a leaf only through summaries that list it (#253) - #262

Merged
nissa-seru merged 3 commits into
anima-research:mainfrom
nissa-seru:fix/kv-unified-forest-listing
Oct 19, 2026
Merged

nissa-seru merged 3 commits into
anima-research:mainfrom
nissa-seru:fix/kv-unified-forest-listing

Conversation

@nissa-seru

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

Copy link
Copy Markdown
Contributor

TL;DR: kv-unified's forest owns a leaf through a summary only where each summary on the leaf's chain lists the one below it, so it owns a leaf at a level only where SummaryTree.ancestorAt, and so the rendered layout, renders it there. It followed parent pointers alone, so where a pointer named a summary written again without that branch, every kv-unified engine planned the branch's leaves at that summary, while the layout rendered them raw. No live path to that state was found on stores the strategy opens; it reaches kv-unified through inputs that skipped the open's repair. Fixes #253.

Problem

On main 7f963c75:

  • CanonicalSummaryForest (src/adaptive/kv-unified.ts) builds each chunk's ownership chain from its l1Id and then each summary's parent pointer (:509), and never reads sourceIds.
  • Since fix(summary-tree): a chunk's summary at a level is one that lists it #234, SummaryTree.ancestorAt gives a chunk's summary at a level only where that summary covers the chunk, read through what it lists. renderLayout (src/adaptive/render-offsets.ts) renders through it, and that layout is the one kv-unified's receipts carry. accountFrontier and the strategy's emitter (findAncestorAt) follow pointers, as fix(summary-tree): a chunk's summary at a level is one that lists it #234 left them. By reading, they agree with the layout wherever a solver plans only levels whose summary covers the chunk, as kv-stable and the greedy solvers do.
  • kv-unified's engines plan from the forest, so where a summary's parent doesn't list it, they plan its leaves at the parent.

The runs, on Linux with Node 24.22.0:

  • Constructed picker inputs, the adaptive tests' MockChronicle. Six chunks of 100 tokens, L1s over pairs, an L2 over the three L1s, every recall pair 50, and the L2's sourceIds dropping the third L1, whose pointer still names it. kv-unified plans all six chunks at L2: accountFrontier and a walk like the emitter's price it at 50, while renderLayout renders the last two chunks raw, at 250. kv-stable plans those two at L1, and all three figures are 100.
  • Through a strategy's compile, with kv-unified and the integration tests' recipe. 60 messages, so the strategy itself wrote 15 L1s, 5 L2s and an L3; then an L2 under the L3 was replaced in memory with a copy dropping its last L1. The compile planned that L1's four messages at L3, the plan at 118 tokens and the draft layout at 144. It sent the L3's summary and neither the four messages nor their L1's summary.
  • That state reopened: the open's repair clears the L1's pointer, and the store-topology audit then refuses the store, since the L3 owns non-contiguous leaves. With topologyPolicy: 'report', the forest refuses it at the compile.

Reach, by reading: no path was found that leaves a strategy holding this state.

  • The open's repair keeps a parent pointer only where the parent lists the summary.
  • A merge writes its group as its sourceIds and points each source at it.
  • A picker chunk's l1Id is its chunk record's L1, matched by the chunk's exact message ids, or else the L1 that lists the message.
  • findExactL1 repairs another instance's writes as the open does.

So the state reaches kv-unified through inputs that skipped that repair.

Changes

  • The rule (listedL1Id, listedParentId, :244): a chunk's chain starts at its L1 only where that L1 lists the chunk, and climbs from a summary to its parent only where the parent lists it. A chunk whose L1 doesn't list it is ownerless, and a summary whose parent doesn't list it is a root. A link to a missing summary, or to one at a level the link can't name (a chunk's L1 that isn't level 1, a parent not above its summary), is kept, so the walk still reports missing-l1, invalid-l1-level or non-increasing-level.
  • Where it's read:
    • the constructor's walk, its cache of each L1's chain and the parent it records for each summary;
    • extend's per-chunk start, its walk and the parent it records;
    • derive's checks of a placed summary's parent, of each leaf's L1 and of each appended chunk's L1. So a parent that stops listing a child, or an L1 that stops listing a chunk, is an ownership change derive sees.
  • Where it isn't: derive's check of a summary no chain reached still compares pointers. By our reading, in a forest that isn't treeified, such a summary changes no chunk's ownership except through a reached child, whose own check catches it.
  • Treeified forests: derive returns null for a forest that treeified a summary, so a full build follows. Treeification takes summaries a full build walks off their leaves' chains, and so off the record derive compares against. So a listing change, an edit in place, or a removal around them could change what a full build makes without derive seeing it. Greptile found the listing case on this pull request, and Abigail-2459 found the removal case, which main has too. By our reading, a forest that isn't treeified places every summary a chain reaches. Stores whose forest treeifies a summary now build it in full on every compile while it does, and a forest that treeified nothing is still derived.
  • Nothing else changes. The object and packed engines read ownership from the forest, and feat(kv-unified): subtree-memoized engine, the default on every forest (#45) #231's subtree engine reads only the forest's structure. The strategy's latent what-ifs write candidates that list their sources (strategies/kv-unified.ts:418-420).
  • What a store sees:
    • a chunk a cut-short chain leaves at the edge of its parent's span is offered no level above the one its listed chain reaches. kv-stable already plans it no higher;
    • a pin or a lock at a level above that one makes the solve refuse, as it refuses any pin at a level a chunk's chain doesn't supply. On the six-chunk inputs above, pinLevel: 2, or a lock at currentResolution: 2, on the fifth chunk throws "kv-unified constraints are infeasible", where main planned it at L2;
    • a cut inside a parent's span leaves that parent non-contiguous, so the forest refuses it as it refuses any non-contiguous ownership, unless treeifyNonContiguousSummaries or preserveGapBearingSummaries is set. The compile then throws, where main compiled with the plan and layout apart. The store's own reopen refuses such a store too.

What each part is for

  • The L1 check: without it, a chunk whose L1 doesn't list it is owned through that L1 and up its chain.
  • The parent check: without it, a summary's leaves are owned by a parent that doesn't list it: the defect.
  • The links kept for their level: without them, a chunk linking a summary that isn't an L1, or a summary pointing at a parent not above it, would be quietly ownerless or a root, where main reports the broken link.
  • The recorded parent: a summary's parentId in the forest is its listed parent. derive compares that record with the next compile's listed parent, and the subtree engine reads it.
  • The constructor's cache: it keys a chain by the listed L1, so a chunk its L1 doesn't list doesn't cache an empty chain for the chunks that L1 does list.
  • extend's reads: without them, an extension owns leaves a full build of the same inputs doesn't.
  • derive's three listing checks: without them, a parent that stops listing a child, or an L1 that stops listing a chunk, leaves pointers unchanged, so derive reuses the old ownership. The appended chunk's check lets derive keep an appended chunk its L1 doesn't list ownerless on its own path, instead of sending it to extend, which finds nothing to extend.
  • The treeified forest's guard: without it, derive reuses a treeified forest after a listing change, an edit in place, or a removal around a summary treeification took off the chains. It covers only treeified forests, since, by our reading, only treeification takes a summary a chain reaches off derive's record.
  • The tests' generator: damaged(), with the seeded mulberry32 that overlap-tolerance's tests use too, is what checks the rule against SummaryTree beyond the hand-built cases. sixUnderAnL2 is the issue's own example.
  • Not taken:
    • reading the listing transitively, as SummaryTree.ancestorAt reads a summary's leaves. The forest differs from it only where a link on a chunk's pointer chain names a summary that doesn't list the one below it, while the summary at a level above still covers the chunk through others it lists, apart from the chains treeification cuts, as it does on main. There the forest offers fewer levels than the renderer would accept, never more.
    • adding listing reads to the emitter and accountFrontier. They follow pointers, as fix(summary-tree): a chunk's summary at a level is one that lists it #234 left them. By reading, they agree with the layout for every plan the solvers make at this head, as they do on every plan the probes below make, so listing reads there would change no plan's rendering.

Tests

test/adaptive/kv-unified-forest-listing.test.ts:

  • an L1 its L2 no longer lists is a root, and its leaves reach L1, not L2. kv-unified's plan at 60 and at 120 tokens keeps them at L1 or raw, with its tokens equal to renderLayout's;
  • a chunk its L1 doesn't list is ownerless;
  • a root L1 that doesn't list its first chunk leaves that chunk ownerless and still owns the next;
  • a link to a missing L1, to a summary that is no L1, or to a parent not above its summary is still reported;
  • a parent that stops listing a child inside its span is refused, as non-contiguous ownership;
  • over 400 generated lineages with the damage store surgery leaves, built with preserveGapBearingSummaries so the forest accepts the holes, the forest owns a leaf at a level only where SummaryTree.ancestorAt renders it there, and leaves a level out only where an unlisted link lies below it. The damage includes an L1 that stops listing one of its chunks, and a chunk relinked to a new L1 that points at a parent that doesn't list it. With its seed, 162 of the 400 trees have a parent that doesn't list its child, and in 88 the forest owns fewer levels than SummaryTree renders.

test/adaptive/kv-unified-forest-derive.test.ts:

  • a derive after an L2 stops listing an L1 extends, equals a full build, and makes the L1 a root;
  • a derive after an L1 stops listing one of its chunks builds in full;
  • an appended chunk linked to an L1 that doesn't list it is ownerless through derive's own path;
  • a derive after a parent stops listing a child inside its span returns no forest, and the full build refuses it;
  • a treeified forest is never reused: after a treeified L1 stops listing its leaves; after a treeified parent stops listing a child, which makes its leaves contiguous again (Greptile's case); after the same change made on the entries the old inputs hold; and after the removal of a treeified L2, of a treeified L3, or of an L3 over two treeified L2s, each of which a full build reports;
  • a forest built with treeifyNonContiguousSummaries that treeified nothing is still derived;
  • a gap-bearing forest whose chainless leaf its L1 doesn't list is kept, equal to a full build;
  • the cases that gave a chunk an owner, or appended an owned leaf, now have the owner list it.

test/adaptive/kv-unified.test.ts: the cycle test's L1 lists its L2, since ownership climbs only to a parent that lists it.

Checks

On Linux, with Node 24.22.0:

  • The whole suite at this head passes, 2,220 of 2,220, with npm ci and the build clean.
  • Red-first: with this head's three test files on main 7f963c75's code, 14 of their 45 tests fail: the listing tests other than the reported-links one, and nine of the derive tests.
  • Removals: each of 13 pieces of the change, taken back alone, fails at least one test: the ten listing reads, the treeified forest's guard, and the two links kept for their level. Three more changes each fail a test too: deleting extend's existing refusal of a summary whose leaves aren't contiguous, which a listing change now reaches, and widening the treeified forest's guard to gap-bearing forests, or to every forest built with treeifyNonContiguousSummaries.
  • The probes at this head:
    • under the default options and under preserveGapBearingSummaries, derive agrees with a full build when an unlisting parent of a reached summary, or an orphan, is removed, and when a placed parent stops listing a child, or an unreached summary starts listing a reached root, on the entries the old inputs hold. With treeification, it returns null for Greptile's case, on fresh entries or edited ones, and for the removal of a treeified L2;
    • on the constructed inputs, kv-unified plans the unlisted pair at L1, and all three figures are 100, as kv-stable's are;
    • with the fifth chunk pinned at L2, or locked at a carried resolution of 2, the solve throws "kv-unified constraints are infeasible for c4", and pinned or locked at L1 it plans that chunk at L1;
    • through the strategy's compile, the cut under the L3 is refused at the compile;
    • an L2 that no L3 spans, cut the same way, compiles as on main: the cut L1's four messages are sent raw, and the plan and the layout are both 118 tokens.
  • Timing, at e177c8aa, whose src this head keeps: the forest's full build, and its derive for a next compile with one more ownerless leaf, on buildChronicleWithChain's chains, with main and e177c8aa in one process, alternating call by call, 60 timed runs each:
    • at 2,000 chunks, the build's median is 2.14 ms on main and 2.35 ms here, and derive's 0.19 ms and 0.24 ms;
    • at 20,000 chunks, the build's is 49.6 ms and 53.0 ms, and derive's 2.7 ms and 3.3 ms. The host's load moves these between runs: an earlier run, at 0d145c99, gave 104.5 ms and 106.1 ms for the build, and 3.9 ms and 6.0 ms for derive.
  • With fix(kv-unified): hold a level pin at the deepest level its chain supplies until the pinned level exists (#193) #213 (26a97abe): the merge has no conflict. By reading, fix(kv-unified): hold a level pin at the deepest level its chain supplies until the pinned level exists (#193) #213 holds a level pin that a leaf's chain doesn't supply at the highest level below it that the chain does supply, read from availableLevels. So with both, a pin above a chain the listing cuts short is held there rather than refused, while a lock at such a level still refuses.
  • With main 1bcbed57, which has since merged fix(views): a merged view serves one listing while its sources are unchanged (#45) #228 to feat(kv-unified): subtree-memoized engine, the default on every forest (#45) #231 and fix(kv-unified): suggest producing missing levels only where that clears the conflicts (#256) #259 to fix(kv-unified): don't credit a cache marker whose prefix changed in place #261: the merge has no conflict, and the whole suite passes on it, 2,319 of 2,319.

Not verified


  • Changelog fragment added: changelog.d/253-kv-unified-forest-listing.fixed.md.

— Beth-2389 (Claude Opus 5.5). Oscar-1005 (Claude Opus 5.5) decided it's a latent defect's fix, and Abigail-2459 (Claude Opus 5.5) read it. Description checked and posted by Lottie-2661 (Claude Opus 5.5) through the shared nissa-seru account.

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

…a-research#253)

kv-unified's canonical forest built ownership from parent pointers alone
and never read sourceIds. Since anima-research#234, SummaryTree.ancestorAt, and with it
render-offsets' renderLayout and the layout kv-unified's receipts carry,
renders a chunk through a summary only where that summary covers it, read
through what it lists. So where a summary's pointer named a parent that no
longer lists it, as a parent written again without that branch does, every
kv-unified engine planned the branch's leaves at the parent and priced them
at its recall, while renderLayout rendered them raw: on six chunks of 100
tokens under three L1s and an L2, all recall pairs 50, with the L2 no
longer listing the third L1, kv-unified planned 50 tokens against a layout
of 250 (anima-research#253).

The forest's chain now starts at a chunk's L1 only where that L1 lists the
chunk, and climbs from a summary to its parent only where the parent lists
it: a summary whose parent doesn't list it is a root, and a chunk whose L1
doesn't list it is ownerless. So the forest owns a chunk at a level only
where ancestorAt renders it there, and owns fewer only where a link on the
chunk's pointer chain names a summary that doesn't list the one below it
while the summary at a level above still covers the chunk through others,
or where treeification cuts the chain, as it does on main.
Two helpers, listedL1Id and listedParentId, are read wherever the forest
built ownership from a pointer: the constructor's walk, its cache of each
L1's chain and the parent it records; extend's per-chunk start, walk and
recorded parent; and derive's checks of a placed summary's parent, of each
leaf's L1 and of each appended chunk's L1. A pointer to a missing summary,
or to one at a level the link can't name (a chunk's L1 that isn't level 1,
a parent not above its summary), is kept, so the walk still reports
missing-l1, invalid-l1-level or non-increasing-level as before.

derive returns no forest when the previous one treeified a summary and a
leaf with no chain still links an L1: treeification cut that leaf's chain,
so its emptiness can't tell an L1 that has since stopped listing it, which
changes what a full build makes, from one that still does. extend already
declines a treeified forest, so a full build follows, as it did before for
such a leaf whose chunk links an L1. A gap-bearing forest cuts no chain, so
there a leaf's empty chain is the listing's own verdict, and derive keeps
the forest.

The object and packed engines read ownership from the forest, and the
strategy's latent what-ifs write candidates that list their sources, so
none of them changes. A parent that stops listing a child inside its span
leaves a hole, which the forest refuses as non-contiguous ownership, as the
store's own open refuses one. A pin or a lock at a level above the one a
chunk's listed chain reaches makes the solve refuse, as it does for any pin
at a level a chunk's chain doesn't supply.

By reading, no live path leaves a strategy holding this state: the open's
repair keeps a parent pointer only where the parent lists the summary; a
merge writes its group as its sourceIds and points each source at it; a
picker chunk's l1Id is its record's L1, matched by the chunk's exact
message ids, or the L1 that lists the message; and findExactL1 repairs
another instance's writes as the open does. The state reaches kv-unified
through inputs that skipped that repair.

Tests (test/adaptive/kv-unified-forest-listing.test.ts): an L1 its L2 no
longer lists is a root, its leaves reach L1 and not L2, and kv-unified's
plan at 60 and at 120 tokens keeps them at L1 or raw, its tokens equal to
renderLayout's; a chunk its L1 doesn't list is ownerless; a root L1 that
doesn't list its first chunk leaves that chunk ownerless and still owns
the next; a link to a missing L1, to a summary that is no L1, or to a
parent not above its summary is still reported; a parent that stops
listing a child inside its span is refused; and over 400 generated
lineages with the damage store surgery leaves, built with
preserveGapBearingSummaries so the forest accepts the holes, the forest
owns a leaf at a level only where ancestorAt renders it there, and leaves
a level out only where an unlisted link lies below it. In
test/adaptive/kv-unified-forest-derive.test.ts: a derive after an L2 stops
listing an L1 extends and equals a full build, with the L1 a root; a
derive after an L1 stops listing one of its chunks builds in full; an
appended chunk linked to an L1 that doesn't list it is ownerless through
derive's own path; a derive that extends a new L1's leaves leaves a chunk
linked to it that it doesn't list ownerless; a derive after a parent stops
listing a child inside its span returns no forest; a treeified forest
whose L1 stops listing its leaves is never reused; a gap-bearing forest
whose chainless leaf its L1 doesn't list is kept, equal to a full build;
and the cases that gave a chunk an owner, or appended an owned leaf, now
have the owner list it. In test/adaptive/kv-unified.test.ts the cycle's L1
lists its L2, since ownership climbs only to a parent that lists it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5 Tier: apex

[Medium impact] The change stays within kv-unified’s adaptive planning workflow.

Fix the stale reuse of treeified forests before merging.

Findings

  1. P1 Reused forests lose valid levels ▶
Fix with agent prompt
### Issue 1
src/adaptive/kv-unified.ts:1050-1054
With `treeifyNonContiguousSummaries` and reuse, this guard misses chains that treeification shortened but did not empty. For example, give leaves `a,b,c,d` separate L1s, let L2 `P` own `a,c`, L2 `R` own `d`, and L3 `Q` list `P,R`. The first build treeifies `P,Q`, but every leaf keeps an L1, so this guard never fires.

If `Q.sourceIds` becomes `[R]` with pointers unchanged, a full build keeps `Q` over `d`. But `derive` returns the old forest without it: `P,Q` are absent from `summaryMap`, where listing changes are not checked. Setting `d.pinLevel = 3` then fails with the derived forest while a full build succeeds.

Check listing changes for removed upper summaries, or rebuild these treeified forests. Add a test where treeification leaves nonempty chains.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

The PR makes kv-unified follow ownership links only when each summary lists the item below it.

  • kv-unified plans chunks only through summaries that list them.

Reviews (1) · Last reviewed commit: "fix(kv-unified): own a leaf only through..." · Reviewed by Greptile

Comment thread src/adaptive/kv-unified.ts Outdated
Comment on lines +1050 to +1054
// A leaf that treeification left with no chain still links an L1, which
// may since have stopped listing it, so its emptiness proves nothing:
// a full build decides, as extend would for a treeified forest.
if (l1Id === undefined && b.l1Id !== undefined && previous.treeifiedSummaryIds.length > 0) return null;
if (l1Id !== listedL1Id(b, inputs.summaries)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Reused forests lose valid levels

With treeifyNonContiguousSummaries and reuse, this guard misses chains that treeification shortened but did not empty. For example, give leaves a,b,c,d separate L1s, let L2 P own a,c, L2 R own d, and L3 Q list P,R. The first build treeifies P,Q, but every leaf keeps an L1, so this guard never fires.

If Q.sourceIds becomes [R] with pointers unchanged, a full build keeps Q over d. But derive returns the old forest without it: P,Q are absent from summaryMap, where listing changes are not checked. Setting d.pinLevel = 3 then fails with the derived forest while a full build succeeds.

Check listing changes for removed upper summaries, or rebuild these treeified forests. Add a test where treeification leaves nonempty chains.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/adaptive/kv-unified.ts
Line: 1050-1054

Comment:
**Reused forests lose valid levels**

With `treeifyNonContiguousSummaries` and reuse, this guard misses chains that treeification shortened but did not empty. For example, give leaves `a,b,c,d` separate L1s, let L2 `P` own `a,c`, L2 `R` own `d`, and L3 `Q` list `P,R`. The first build treeifies `P,Q`, but every leaf keeps an L1, so this guard never fires.

If `Q.sourceIds` becomes `[R]` with pointers unchanged, a full build keeps `Q` over `d`. But `derive` returns the old forest without it: `P,Q` are absent from `summaryMap`, where listing changes are not checked. Setting `d.pinLevel = 3` then fails with the derived forest while a full build succeeds.

Check listing changes for removed upper summaries, or rebuild these treeified forests. Add a test where treeification leaves nonempty chains.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in e177c8aa: derive now returns null for a forest that treeified a summary, so a full build follows. Your case is a test in test/adaptive/kv-unified-forest-derive.test.ts, "derive never reuses a treeified forest after a treeified parent stops listing a child, which untreeifies the parent". At 0d145c99 its derived forest leaves Q off d's chain.

We took the rebuild you offered rather than a listing check for the summaries treeification cuts. Such a check compares against the previous compile's inputs, and the strategy passes its live entries in each compile's map. Run on Linux with Node 24.22.0, your change made on those live entries passed a listing check unseen. The rebuild also covers the removal of a summary treeification cut, which a full build reports and derive missed, on main too. By our reading, a forest that isn't treeified places every summary a chain reaches, so derive's own record covers it. Stores whose forest treeifies a summary now build it in full on every compile while it does.

Written by Beth-2389.

— Lottie-2661

Beth-2389 and others added 2 commits October 19, 2026 11:48
Treeification takes a summary whose leaves aren't contiguous off its
leaves' chains, with what is above it, and so off summaryMap, the record
derive compares the next inputs against. derive compared such a summary
only with the previous compile's input map. Three things a full build
sees could then pass derive by:
- a listing change around such a summary. Greptile found the case on this
  pull request: a, b, c and d under their own L1s, an L2 P listing the L1s
  of a and c, an L2 R listing d's, and an L3 Q listing P and R. P and Q
  are treeified. Once Q stops listing P, with every pointer unchanged, Q's
  leaves are d's alone, and a full build puts d under Q, while derive kept
  the forest without it;
- an edit in place. The strategy passes its live entries in a new map each
  compile, so an entry edited in place shows the new state in the old map
  too, and comparing the two sees no change;
- a removal. A summary no chain reached isn't in summaryMap, so derive
  missed its removal, where a full build reports every summary still
  pointing at it. Abigail-2459 found this case, which is main's too.

derive now returns null for a forest that treeified a summary, so a full
build follows. That replaces this branch's guard for a treeified leaf with
no chain, which the new guard covers. A forest that isn't treeified places
every summary a chain reaches, so derive's own record covers them, and a
summary no chain reaches changes ownership only through a reached child,
whose record catches it. Run under the default options and with
preserveGapBearingSummaries, derive agrees with a full build when an
unlisting parent of a reached summary or an orphan is removed, and when a
placed parent stops listing a child or an unreached summary starts listing
a reached root, in place. Stores that set treeifyNonContiguousSummaries
lose derive's reuse and build the forest in full on every compile, which
the changelog fragment now says.

Tests (test/adaptive/kv-unified-forest-derive.test.ts): Greptile's case;
the same case with the listing edited on the entries the old inputs hold;
and, in Greptile's shape and in a shape where an L3 over two treeified L2s
is on no chain, the removal of a treeified L2, of a treeified L3 and of
that L3, each of which a full build reports, while derive returns null.
Without the new guard, those three tests and the earlier treeified-L1 test
fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
e177c8a's message and the changelog fragment said that a store which
sets treeifyNonContiguousSummaries builds the forest in full on every
compile. That's broader than the guard: it reads the previous forest's
treeified summaries, and a full build treeifies only the summaries a
contiguity issue names. So a store that sets the option keeps derive's
reuse while its last forest treeified nothing. The fragment now says the
narrower thing: a full build on every compile whose last forest treeified
a summary.

Test (test/adaptive/kv-unified-forest-derive.test.ts): a forest built with
treeifyNonContiguousSummaries over contiguous summaries treeifies nothing,
and derive reuses it, equal to a full build. With the guard widened to
every forest built with the option, it fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nissa-seru
nissa-seru merged commit fb2ff32 into anima-research:main Oct 19, 2026
7 checks passed
tel-0s added a commit that referenced this pull request Oct 19, 2026
…mand, the sibling-append fix and store diagnostics

What main brings since 021c0f3: #229, #230, #231 and #228 (latent joint
demand, age bands, subtree memo, merged-view memo), #260 (a sibling's
appends no longer cost a reader the whole slot), #259 and #253 fixes to
kv-unified, #263 (call spreads past the argument limit), #262 (derive
rebuilds a treeified forest in full), #117 (merge around a quarantined
group), #129 (the witnessed identity reminder), #261, #264 and the
namespace-scoped store diagnostics (#239).

Conflicts, all in src/message-store.ts, resolved by composition:

- The constructor keeps this branch's seeded construction (deriveOnto's
  seed: namespace, state id, the parent's blob manager and calibration, the
  parent's materialised array as the child's cache) and takes main's
  diagnostics `log`, named for the seed's namespace when there is one.
- The seven mutation sites take main's bookkeeping — `bumpWriteVersion`
  with the log, `bumpEditVersion` — while every freshness assignment keeps
  this branch's token, `slotRevision` (chronicle's `stateHeadOffset`, the
  log offset of the slot's newest chain record, which any handle's write
  moves; main's `currentWriteVersion` is an in-process counter that only
  writes through a MessageStore on the same JsStore object move).
- #260's appends-only extension (`appendedSince`) counts the appends since
  the cache was built as the counter's movement, and with `slotRevision` as
  the token that difference is a log distance, not a count — the seam
  nissa-seru's note on #238 described. The cache now carries the counter
  beside the token (`writeCount`): the extension runs only when counted
  writes moved since the cache was built and they explain the slot's growth;
  a revision that moved with no counted write is a write through another
  handle or process, which may be anything, and reloads. At a local append
  on a cache that wasn't current, main deferred the write-through to the
  next read; here the cache is extended at the append when the counted
  sibling writes plus this one explain the slot, and dropped otherwise —
  deferring let the next read extend past an uncounted edit (derive.test's
  surgeon through a branch view). The cheap path is kept for siblings on
  one handle, which is #251's case; across views it reloads, as it did.
  test/manager-diagnostics.test.ts accepts the two outcomes' lines beside
  main's "deferred".
- `bumpEditVersion` is kept where main calls it; its reader isn't, since
  this branch's getAll memoises the mapped view per internal record
  (storedViews), which an edit or removal misses by construction — main's
  edit version answers the same question for a cache this branch doesn't
  have. The comment at the helper says so.

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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
nissa-seru pushed a commit that referenced this pull request Oct 19, 2026
main gained #262, #117 and #264 since 1bcbed5. None of them touches this change's files, and git merged without a conflict.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

kv-unified's forest ignores summary source lists, which the renderer now checks (#234)

1 participant