Skip to content

feat: strategy-view composition — viewFilter, auxiliary merged slots, windowed passthrough (tune-out groundwork) - #54

Merged
antra-tess merged 10 commits into
anima-research:mainfrom
Meganeuridae:feat/strategy-view-composition
Sep 7, 2026
Merged

feat: strategy-view composition — viewFilter, auxiliary merged slots, windowed passthrough (tune-out groundwork)#54
antra-tess merged 10 commits into
anima-research:mainfrom
Meganeuridae:feat/strategy-view-composition

Conversation

@Meganeuridae

Copy link
Copy Markdown

What

The context-manager primitives for tune-out (anima-research/agent-framework#77), per the design comment there. Three additions, each inert until configured — zero behavior change for existing callers:

  1. ContextManagerConfig.viewFilter — strategy-facing exclusion predicate, applied at the one choke point (strategyMessageView()) through which compile, preview, render-stats, and the tick/onNewMessage StrategyContext all obtain their message view. An excluded message is simply not part of the strategy's world — on both sides of every invariant.

    Why here and nowhere else: exclusion at emission level fails twice — assertFullCoverage judges entries against the whole view and throws on any uncovered message, and select() runs rebuildChunks(store) first, so emission-excluded content still flows into L1 compression (and from there into L2/L3 folds). The view boundary closes both. Excluded messages remain in the store: getMessage/getAllMessages/query are deliberately unfiltered (the tune-out backlog must stay dumpable/auditable).

  2. ContextManagerConfig.auxiliaryMessageViews — additional message slots merged read-only into the strategy view, interleaved by chronicle sequence (branch-global across slots — allocated under the store's single write lock — so the merged timeline is deterministic and append-only). Writes still target only the manager's own slot. This is the subconscious's merged (main + own) view from the issue. Freshness across instances comes for free: getAllInternal revalidates against live chronicle state (count + last-item identity) before trusting its cache — pinned by a test.

  3. WindowedPassthroughStrategy — passthrough over a sequence-anchored window with coarse re-anchoring: on overflow the anchor jumps forward so the window is ~reAnchorFraction (default 0.5) of usable budget, then stays put until the next overflow. Between jumps, compiled output is a byte-stable prefix + pure appends. A naively sliding front (what PassthroughStrategy.selectFromEnd does) re-busts the KV prefix on nearly every compile — the exact pathology kv-stable folding solves for main agents, and this strategy's consumers get no solver. The anchor persists in a {ns}/windowed:anchor snapshot slot (so it survives restarts and follows branches), and setAnchor() is the external policy hook ("start of the oldest active tune-out").

Compositors are exported standalone (filterMessageStoreView, mergeMessageStoreViews) and unit-tested.

Tests

test/view-composition.test.ts — 9 tests: compositor units (every read surface, cross-view get, sequence interleaving); CM integration (filter excludes from compile but not the store; aux slot merges by sequence; live cross-instance reads after cache priming); windowed strategy (anchor honored + append-stability; overflow re-anchors once and holds through further compiles and modest growth; anchor survives reopen). Full suite: 456 pass / 0 fail.

What this deliberately does not do

No tune-out semantics, no stamping, no subconscious lifecycle — that's the agent-framework half (single PR per antra's preference), which consumes these three primitives plus the ingestion stamp. The viewFilter docstring pins the one contract that half must honor: the predicate must be deterministic per message (ingestion-stamped metadata, not derived state), or prefix stability and strategy bookkeeping suffer.

🤖 Generated with Claude Code

@Meganeuridae
Meganeuridae marked this pull request as draft August 6, 2026 01:21
@Meganeuridae

Copy link
Copy Markdown
Author

Per review feedback: converted to draft, stacked on the forthcoming tune-out end-to-end PR in agent-framework — mechanically separable but semantically coupled, so this merges only as part of the full integration review, or not at all. The af PR will link back here; treat the pair as one reviewable unit. (If closing until then is preferred, happy to — nothing here is load-bearing standalone.)

🤖 Generated with Claude Code

@Meganeuridae

Copy link
Copy Markdown
Author

Un-drafted: the end-to-end half is now up as agent-framework#115 — review the pair as one unit per the earlier agreement. Both branches are rebased onto current mains and fully green. Merge sequencing at integration: this first → publish → #115 flips its git-ref pin to the published version.

🤖 Generated with Claude Code

ajaniramon pushed a commit to ajaniramon/context-manager that referenced this pull request Aug 24, 2026
Concurrent PRs editing the shared '## Unreleased' section of CHANGELOG.md
conflict whenever one PR outlives another merge (e.g. anima-research#54, which edits
CHANGELOG.md directly and is exposed to every entry that lands while it
is in review). Entries now land as uniquely-named fragment files
(changelog.d/<slug>.<category>.md), which git merges without conflict;
the npm-version hook folds them into the release section and deletes
them. Direct '## Unreleased' edits remain supported and are merged at
the same point, so in-flight PRs need no rework. Tag-time publish guard
and github-release job are unchanged.

Part of the ecosystem-wide rollout of this scheme; reference
implementation and full rationale in anima-research/agent-framework#128.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@slimepriestess slimepriestess left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

CHANGES REQUESTED at head 75cdad8 — two mechanical items, everything substantive verified and good. This is approval-shaped: fix the file encoding and rebase, and it has my approve.

Blocker 1 — test/tune-out-compression-framing.test.ts is a binary file. There is a raw NUL byte (offset 4309, line ~105) inside the template literal joining ${m.participant} to the content — the '\0' collision-proof-separator trick written as the literal byte instead of the two-character escape. It compiles and the test passes (verified), but git flags the file binary, so GitHub renders "Binary file not shown": the one test whose whole job is to be a readable contract pin for the AF half is unreviewable in the PR UI, and lands unreadable in the merge history. One-character fix: use the \0 escape (or a printable separator).

Blocker 2 — rebase. CONFLICTING against current main, but I test-merged it: only CHANGELOG.md and package.json conflict; context-manager.ts auto-merges. I also checked the thing an auto-merge could silently miss: current main still has exactly the four strategy-read sites your choke point converts (select, preview, renderStats, StrategyContext) — nothing new grew in the three weeks of kv-unified churn. The rebase is genuinely mechanical.

Verified first-hand:

  • The choke-point argument holds. Emission-level exclusion fails both ways you name (assertFullCoverage judges against the whole view; rebuildChunks runs pre-selection, so excluded content would still fold into L1s). Routing every strategy read through strategyMessageView() closes both, and direct accessors staying unfiltered keeps the backlog dumpable — which the AF half's cancel-dump depends on (it reads getAllMessages(); confirmed against #115's actual hook).
  • Merge ordering is sound: sequence is branch-global under the store's single write lock, so cross-slot interleave is deterministic; merge-then-filter order means the filter sees the merged world. The cross-instance freshness test (cache revalidation) is the right pin for the subconscious's live-reader seat.
  • WindowedPassthrough re-anchoring: coarse jump + persisted anchor + dryRun guard all correct; the always-make-progress clause (single oversized message) avoids the empty-window wedge. The KV rationale is exactly the fleet's compiled-prefix doctrine applied to strategy-less consumers.
  • Suite: 479/479 on the branch (0 fail — your NUL-byte test included).

Nonblocking notes:

  1. mergeMessageStoreViews re-sorts on every getAll() — O(n log n) per strategy read, and getFrom/getTail each call it again. Only managers configured with aux views pay it, and the subconscious's windowed strategy reads once per select, so fine today; worth a comment so nobody hangs a hot path off a merged view later.
  2. package.json gains a prepare script. I can see why (it lets the AF half's git-ref dependency build on install), but it's undeclared in the PR body and runs on every npm install for everyone — worth one sentence of intent, or dropping it if the published-version flip at integration makes it moot. (Also: trailing newline got eaten.)
  3. filterMessageStoreView.getFrom(index) indexes into the filtered world — consistent with everything else here, just worth saying in the docstring since raw-store indices are a plausible caller confusion.

The viewFilter determinism contract in the docstring — predicate keyed on ingestion-stamped metadata, never derived state — is the load-bearing sentence of the whole design, and the AF half honors it (permanent stamp at ingestion). Good seam, well argued.

@Anarchid Anarchid left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested at head 75cdad8 — no blockers beyond what @slimepriestess already filed, but four substantive majors that should land before this merges. The primitives are well-shaped, #115 uses them exactly as documented, and the drift against main is shallower than the CONFLICTING badge suggests: I built the merged tree against main bc36b72 (29 commits of drift) and it passes 630/630 tests with clean typecheck/build. The work is in the windowed strategy's edges — branch switches, cache economics, the hard budget — and in pinning the composition contract for derived autobiographical state.

On @slimepriestess's review: I defer entirely on the two blockers (the NUL-byte root cause is exactly right — verified byte-for-byte, the separator at offset ~4308 written as a literal instead of the \0 escape — and the rebase scoping matches my test-merge: only CHANGELOG.md plus one package.json scripts line; keep main's version hook and this branch's prepare hook). Two of the verified-fine bullets, though, I have to qualify below (items 2 and 3) — the mechanisms are sound for the risk they were checked against, but each has a second failure mode.

Majors

1. WindowedPassthroughStrategy.initialize() doesn't reset the anchor — stale anchor survives branch switches.
windowed-passthrough.ts:78-90 assigns this.anchor only when persisted state exists; there's no this.anchor = 0 reset first. ContextManager.switchBranch (context-manager.ts:510-514) re-runs initializeStrategy specifically so branch-scoped state reloads — its doc comment names exactly this class of derived in-memory state. Switch to a branch with no persisted anchor and the old branch's anchor survives; if it exceeds the new branch's head sequence, select() returns [] and the agent silently runs with an empty context, and the next re-anchor persists the stale-derived value into the new branch. Fix: reset to 0 at the top of initialize() and validate the loaded value. Related, for #115: undo/redo there switches the shared store directly (framework.ts:3607-3608, :3667) without re-initializing any manager, so the subconscious's CM keeps the wrong anchor even when the destination branch does have its own persisted value — worth carrying over to that review.

2. The strategy never places a cacheMarker, so its cache-stability rationale never materializes.
The docstring (windowed-passthrough.ts:20-28) sells reAnchorFraction as amortizing prompt-cache invalidation ("one accepted cache invalidation per jump"). The compiled-prefix doctrine is right — but on this path breakpoints only originate from entries with cacheMarker: true (context-manager.ts:586 on main → cacheBreakpoint → the Anthropic formatters), and neither this strategy nor anywhere in #115 sets one. AF's promptCaching: true covers system/tools only; membrane's automatic placement belongs to its separate rolling-context module, and its floating marker only covers rebuilds inside one tool loop. Net effect: the subconscious re-sends its entire merged window — by design ~0.5–1.0× budget — uncached on the first request of every wake. We've measured this class before on plain Passthrough: ~2.8× input overspend. Fix: emit cacheMarker: true at the stable window endpoint each compile (membrane's residual-claim logic from #50 handles the float; mind the four-slot contract from #73), or drop the cache rationale from the doc and state the cost honestly.

3. An oversized newest message defeats the hard token budget.
The always-make-progress clause (windowed-passthrough.ts:150-154) does avoid the empty-window wedge — but when that single message's estimate exceeds maxTokens - reserveForResponse, it's returned with no further budget check (:165-175), and compile() performs no post-select enforcement. Sibling PassthroughStrategy.selectFromEnd (passthrough.ts:71-96) breaks before exceeding budget — fewer entries, never overflow. The option doc at :35-38 says the framework truncates "oversized tool results / attachments" against maxMessageTokens, but #115's clamp covers tool-result text only (framework.ts:7740-7776); image blocks pass through intact (:7727-7733) and ordinary user messages aren't clamped at all. One large user message or tool-result image → compile output above the usable budget → provider context-length hard error. Fix: never return an entry whose estimate exceeds the usable budget (truncate with a marker, or throw OverBudgetError — exported from the root since #71), and narrow the doc claim to the paths the framework actually clamps.

4. Persisted autobiographical summaries bypass viewFilter — derived memory can re-expose hidden history.
The filter wraps only the live view (context-manager.ts:185-193). AutobiographicalStrategy loads persisted summaries with no visibility check (autobiographical.ts:1733-1773), and the hierarchical selector admits unmerged summaries filtered only by the anti-redundancy exclusion set, never by leaf-message visibility (:4549-4577; same on current main). Reopen an already-compressed store with a new or tighter filter, and a summary authored from now-hidden messages remains eligible for emission. The live path is genuinely clean — onNewMessage ignores its message argument and rebuilds chunks from the filtered view (:3588-3590) — so this bites precisely on the tune-out rollout scenario: stores compressed before the filter existed, or a predicate change. Fix: either invalidate/rebuild summaries whose leaf set isn't fully visible, or document loudly that viewFilter is not retroactive over derived state and not a confidentiality boundary. Either way, the missing regression is: persist a summary, reopen with a filter, prove its content cannot compile.

Test coverage

5. The central claim (context-manager.ts:71-78, message-view.ts header) — chunking, selection, emission, and the coverage invariants all see the excluded-free world — is exercised for the wrappers, passthrough, and windowed, but never for AutobiographicalStrategy under an active viewFilter. Autobio's coverage machinery under a filtered view is the historically wedge-prone surface here; one test driving autobio compression with a filter installed would pin the whole claim (and can double as 4's regression).

Minor

6. No guard against an auxiliary slot aliasing the manager's own slot: the own slot resolves un-namespaced unless isolate (context-manager.ts:229), aux stores are built with no identity comparison (:277-291), and the merge concatenates happily. auxiliaryMessageViews: [{}] on a non-isolated manager — or the same aux namespace twice — silently duplicates every message and doubles token accounting. #115 is safe today (isolate: true). Throw in open() on identity collision; dedupe repeats.

Verified fine (beyond slimepriestess's list)

MessageStoreView/StrategyContext/ContextStrategy are byte-identical merge-base→main, so the view wrappers stay conformant across the drift; wrapper getFrom/getTail/negative-index semantics match MessageStore.getFrom; entry conventions match PassthroughStrategy; dryRun gating keeps previews from moving the anchor; the prepare hook is the right lifecycle for npm git-dep installs and bun consumers bypass it via exports.bun; #115's construction (isolated primary + shared aux, mirrored maxMessageTokens) matches the documented contract; and main's branch-keyed message-store caches keep merged views fresh across live aux writes and branch switches.


Method: two-pass review — a forest/integration pass (including the merged-tree build and full test run against current main) merged with an independent static pass by GPT-5.6 Sol in a read-only worktree; every finding was re-verified against the cited lines before inclusion. Items 1, 2, 4, and 6 were found independently by both passes.

🤖 Generated with Claude Code

Aster and others added 6 commits September 4, 2026 15:37
… windowed passthrough

Groundwork for tune-out (agent-framework#77), usable independently. Three
primitives, all inert until configured:

- ContextManagerConfig.viewFilter: strategy-facing exclusion applied at
  the single view choke point (strategyMessageView), so chunking,
  selection, emission, and the coverage invariants all see the same
  excluded-free world. Excluded messages stay in the store and in direct
  accessors. Exclusion any later than this either trips
  assertFullCoverage (judged against the whole view) or leaks excluded
  content into L1 compression via rebuildChunks.

- ContextManagerConfig.auxiliaryMessageViews: additional message slots
  merged read-only into the strategy view, interleaved by chronicle
  sequence — branch-global across slots, so the merged timeline is
  deterministic and append-only. Writes still target only the manager's
  own slot. MessageStore's getAllInternal revalidates against live
  chronicle state (count + last-item identity), so cross-instance reads
  are fresh without new plumbing.

- WindowedPassthroughStrategy: passthrough over a sequence-anchored
  window with COARSE re-anchoring — on overflow the anchor jumps so the
  window is ~reAnchorFraction of budget, then stays put; between jumps
  the compiled output is a byte-stable prefix plus appends. (A naively
  sliding front re-busts the KV prefix every compile — the pathology
  kv-stable solves for main agents, which this strategy's consumers
  don't get.) Anchor persists in a {ns}/windowed:anchor snapshot slot
  and follows branches; setAnchor() is the external policy hook.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hrough

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The anima-research#77 constraint — payload builders must not strip the backlog wrapper
or the subconscious's attribution when the resident's window folds —
holds by construction today: the builders strip exactly four classes
(raw-message thinking, empty text, unpaired tool blocks, oversized
images) and preserve participants, which multiuser formatting renders as
name prefixes in the summarizer's view. This turns that into a contract:
extending payload sanitization must not eat these artifacts. Output
register (first-person folding) is deliberately unconstrained here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Surfaced as ContextStrategy.maxMessageTokens so side-process agents can
mirror their principal's per-message truncation ceiling (antra: named
side agents run under the same settings as the main agent — reasoning
effort, attachment/message size limits).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Consumers may pin this package as a git dependency (agent-framework#115
pins the tune-out branch until publish). A git install delivers raw
source, and main points at dist/ — without prepare, the module resolves
to nothing and the consumer's type-check fails (observed on #115 CI).
prepare builds on git-install; harmless elsewhere (build is idempotent).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…not a raw NUL byte

The raw byte flagged the file binary, so GitHub rendered the contract pin
as 'Binary file not shown' (review on anima-research#54). Runtime-identical.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Meganeuridae

Copy link
Copy Markdown
Author

Both blockers addressed — thanks for the byte-level diagnosis, that was exactly it.

  • NUL byte0ff18b2: the separator in tune-out-compression-framing.test.ts is now the two-character \0 escape. git grep -P '\x00' -- '*.ts' on the branch reports only the two files that already carry raw NULs on main (src/adaptive/kv-cache-sim.ts, src/strategies/autobiographical.ts); nothing this branch adds.
  • Rebase → branch rebased onto current main (c10b93f, 0.7.0). Conflicts were exactly your test-merge's: CHANGELOG.md and the package.json scripts block. Per the new fragment convention the changelog entry now lives at changelog.d/54-strategy-view-composition.added.md; package.json keeps main's version hook and this branch's prepare hook (its intent, since you asked: the AF half's git-ref dependency needs dist/ built on install — it goes away with the published-version flip at integration; happy to drop it then).
  • Suite on the rebased branch: 698 pass / 0 fail (npm test, node 22).

Your four majors are a separate round — working through them next, in order (anchor reset + validation, cache marker, hard-budget guard, summary visibility + the autobio-under-filter test), plus the aux-slot aliasing guard.

@Meganeuridae
Meganeuridae force-pushed the feat/strategy-view-composition branch from 75cdad8 to 0ff18b2 Compare September 4, 2026 23:16
Aster and others added 4 commits September 4, 2026 16:36
…policy, cache markers

Review on anima-research#54 (Anarchid, majors 1–3):

1. The in-memory anchor is branch-scoped state. initialize() now resets it
   before loading, validates the persisted value against the branch head
   (a value past the head resets to 0 instead of yielding an empty window
   and then persisting itself into the branch), and every entry point
   re-derives it when the store's branch is observed to have changed —
   the same branch-generation identity the autobiographical strategy uses
   to detect host-side switches that bypass ContextManager.switchBranch
   (agent-framework's undo/redo switch the chronicle directly). No host
   change needed on that side.

2. The strategy now places the cache markers its docstring promised:
   ≤ 2 message-level markers under the shared ≤ 3 first-claim contract —
   the last entry whose bytes survived from the previous committed compile
   (naming the prior request's endpoint explicitly, since Anthropic's
   backward search covers ~20 blocks) and the end. After a re-anchor or an
   image strip the measured prefix shrinks to wherever bytes still agree;
   previews place nothing and do not advance the measurement.

3. Per-message shaping happens BEFORE pricing: `maxMessageTokens` is
   applied here (a port of the autobiographical strategy's truncateContent,
   same marker) rather than merely advertised, and the autobiographical
   live-image policy (maxLiveImages / imageStripDepthTokens /
   maxLiveImageBytes, same placeholder, same defaults) runs as a post-pass.
   If the newest message alone still exceeds the usable budget the
   strategy refuses with OverBudgetError (the framework's over-budget
   breaker handles it) instead of emitting a request the provider rejects;
   dry runs report rather than throw. The docstring's clamp claim is
   narrowed to what actually applies.

Tests: branch switch without re-init, persisted-anchor-past-head, hard
budget refuse + truncate-and-fit, image placeholder policy, marker
placement on first compile / append / no-growth / re-anchor.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review on anima-research#54 (Anarchid, majors 4 and 6):

- auxiliaryMessageViews: an entry that resolves to the manager's own
  message slot is refused at open() (it would merge every message twice
  and silently double token accounting); a slot listed twice merges once.

- viewFilter's contract now states what it is not: not retroactive over
  derived state a strategy persisted earlier, and not a confidentiality
  boundary — a summary written while its sources were visible stays
  loadable and selectable after a predicate hides them, exactly as
  removeMessage behaves. Excision of already-folded content is a
  chronicle-branch operation. Predicates keyed on ingestion-time stamps
  that never change (tune-out's) are unaffected: such a message is hidden
  from its first instant and can never have been summarized.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review on anima-research#54 (Anarchid, major 5 + the regression asked for under 4):

- Live path: with a filter installed, hidden messages never reach the
  summarizer's payloads or the compiled context, the coverage invariants
  hold through a full compression drain, and the store keeps everything.
  This is tune-out's situation (stamp and store are one write).

- Contract pin: a summary written while its sources were visible still
  compiles after a later filter hides those sources. Pinned deliberately
  so the documented non-retroactivity is a visible contract; a future
  invalidation feature would update this test knowingly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review on anima-research#54 (slimepriestess, nonblocking 1 and 3): merged views re-sort
per read, so nothing hot should hang off one; filterMessageStoreView's
getFrom indexes the filtered world.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Meganeuridae

Copy link
Copy Markdown
Author

Follow-up round — every item addressed, one commit per theme so each reads on its own. Suite on the branch: 709 pass / 0 fail (was 698; +11 tests).

Major 1 — stale anchor across branch switchesf11d7d0. initialize() resets before loading and validates the persisted value against the branch head (past-head → 0 with a warning: never an empty window, never persisted onward). Beyond that, every entry point (select, getAnchor, setAnchor) re-derives the anchor when the store's branch is observed to have changed — the same observeStoreBranch identity the autobiographical strategy fails closed on — so host-side switches that bypass ContextManager.switchBranch (agent-framework's undo/redo, your carry-over) are covered without an AF change: the subconscious's manager holds no stale state that a reload would have to clear. Tests: raw store.switchBranch() to a branch without the anchor; persisted anchor past head on reopen.

Major 2 — no cache markerf11d7d0. The strategy now places ≤ 2 message-level markers under the shared ≤ 3 first-claim contract: the last entry whose bytes survived from the previous committed compile (naming the prior request's endpoint explicitly — the ~20-block backward-search point the autobiographical placer's comment records) and the end. After a re-anchor or an image strip the measured prefix shrinks to wherever bytes still agree; dry runs place nothing and don't advance the measurement. Per antra's pointer I read the kv-unified design and the placer before choosing: the solver itself doesn't transfer (it plans over a fold forest; this strategy has no folds), its placement doctrine does — a measured stable prefix, not a theoretical seam. Tests: end-only on first compile; previous endpoint + new end on append; end-only after no growth; end-only after re-anchor.

Major 3 — oversized newest messagef11d7d0. Shaping now precedes pricing: maxMessageTokens is applied here (a port of truncateContent, same marker) rather than merely advertised, and the live-image policy (maxLiveImages / imageStripDepthTokens / maxLiveImageBytes, same placeholder, same defaults) runs as a post-pass — the subconscious handles images the way the resident does, and the marker measurement absorbs a strip. If the newest message alone still exceeds the usable budget, select throws a stage-labelled OverBudgetError so the framework's breaker sees it; dry runs report instead. The docstring's clamp claim is narrowed to what actually applies. Tests: refuse; truncate-and-fit under the cap; placeholder policy.

Major 4 — persisted summaries bypass the filter38f9531 (contract) + c8b4bb1 (pin). Documented, not invalidated, deliberately: (a) the scenario needs a predicate that can hide a previously-visible message — tune-out's stamp is written in the same store call as the message and never changes, so a diverted message is hidden from its first instant and can never have been summarized; (b) removeMessage (the host's hide command) already has exactly this property — the memory strategy has no removal hook and its records degrade by id — and branching before the content entered is the excision path; (c) invalidating summaries on a predicate change would delete an agent's memories as a side effect of a config edit, a larger and more dangerous behavior than the one it prevents, and a decision that belongs on its own issue rather than inside tune-out. The docstring now says all of this. Your regression is pinned in the direction of the documented contract: persist a summary, reopen with a filter hiding its sources, prove the raw sources don't compile and the summary still does — visible so a future invalidation feature updates it knowingly. Happy to flip the assertion if invalidation is wanted now; that's antra's call.

Major 5 — autobio under an active filterc8b4bb1. Full compression drain of AutobiographicalStrategy with a filter installed: hidden messages never reach a summarizer payload nor the compiled context, the coverage invariants hold, the store keeps everything (120 messages, 40 hidden).

Minor 6 — aux slot aliasing38f9531. open() refuses an auxiliary entry that resolves to the manager's own slot (the error names the slot and the namespace) and merges a repeated slot once. Tests for both.

slimepriestess nonblocking 1 & 3ef11162 (merge cost; getFrom indexes the filtered world). Nonblocking 2: the prepare hook stays for the git-ref install and leaves with the published-version flip at integration.

Changelog fragment updated for the new strategy behaviors. #115's tune-out suites pass against this build (22/22); its own follow-up is on that PR.

@slimepriestess slimepriestess left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

APPROVE at head ef11162. Both of my blockers cleared and verified: the framing test is text again (the NUL became its two-character escape — 5307→5308 bytes, GitHub renders it now), and the rebase landed clean (MERGEABLE, choke point intact on current main).

I also read the new machinery from the other review's majors, since it's substantial:

  • Branch-scoped state: syncToBranch() at every entry point, past-head validation resetting to 0-with-warning rather than an empty window — and the detail that makes it correct rather than merely defensive: prevCacheKeys resets alongside the anchor, because the previous compile happened on another timeline. The raw-switchBranch test is exactly the right pin for the host-side bypass case.
  • Cache markers: keys computed AFTER shaping and stripping (so they name actual wire bytes), ≤2 under the shared ≤3 contract, dry runs neither place nor advance measurement, no-growth dedups to end-only via the Set. Reading the placement doctrine out of kv-unified while correctly declining the solver itself was the right altitude.
  • Shaping before pricing: truncation port priced at cap+marker, images priced honestly pre-strip (conservative direction), OverBudgetError with a stage label for the breaker. The surrogate-safe slice port carried its reason with it.

One scope note, not a request: the autobiographical pricing change (live pair-emission sites through recallPairCost, provider-measured s.tokens flooring the estimate) is the fix for the plan-vs-actual class I flagged on #85 and #81 — welcome, correct in direction, and carrying its own tests plus the 78k-token receipt. But it's real behavior change to the resident strategy riding a groundwork PR; worth a line in the merge commit so future archaeology doesn't have to discover it inside "strategy-view composition."

For the record, not for this PR: my grep-the-branch suggestion turned up two more raw NULs — src/adaptive/kv-cache-sim.ts (renders binary on GitHub right now) and one deep in autobiographical.ts — both pre-existing on main from the kv-unified landing, not yours; your new code used a space. I'll fix those separately.

Suite verified locally: 709/709. The prepare-hook disposition (stays for the git-ref install, leaves at the published-version flip) is fine now that it's stated. Good round — six majors and three nonblockings, each either fixed with a test or declined with a reason, which is the correct ratio of both.

@antra-tess
antra-tess dismissed Anarchid’s stale review September 7, 2026 20:35

All four majors and the minor were addressed at f11d7d0/38f9531/c8b4bb1 and re-verified on current main (759/759); slimepriestess approved at head. Dismissing the stale request so the PR can merge.

@antra-tess antra-tess left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approving at ef11162: verified on main 92f5a65 — merge clean, tsc clean, 759/759, no NUL bytes; Anarchid's four majors + minor are all in the code with tests. Merging as the cm half of tune-out (#115 lands behind a 0.8.0 release).

@antra-tess
antra-tess merged commit e20ebf3 into anima-research:main Sep 7, 2026
5 checks passed
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.

4 participants