Repository navigation
feat(view): blocks with an audience, and the reader a manager compiles for - #364
nissa-seru wants to merge 5 commits into
Conversation
…s for A host whose agents share one message slot may need to write a content block meant for some of them only, such as a line naming a message's channel the way one agent's tools take it. Keeping that block inside its message's own row gives it the message's lifetime everywhere (removal, shards, windows, summaries, edits); what's needed is that each reader's view shows it only to the readers it's for. A content block may now carry `audience`, the readers it's for, and `viewReader` names the reader a manager compiles for. The strategy-facing view, after `viewFilter`, keeps a block with no `audience` or one whose `audience` names the reader, without the field, and leaves out a message whose every block is someone else's, as a filtered message is left out. With no `viewReader` it keeps no block that has an `audience`. An `audience` that isn't a list of strings, such as a list with a number in it, names no one. Hosts write such blocks as `ContentBlockWithAudience`, which `addMessage` and `editMessage` take. Everything a compile reads or reports for a reader is that reader's view. Every strategy read that reaches content goes through the view: select, renderAsNewest, the summary listing, render stats, the ticks that chunk and summarize, and onNewMessage's message, which is the reader's view of the new message (whatever the view filter says, as on main) and isn't offered when the reader sees nothing of it; pins hold ids and are rendered at select, and the view's sequenceOf answers as its get does. Compile attribution judges raw bodies by the reader's view of the unfiltered store, so rawBodies holds the reader's objects and a block another reader's audience hides isn't counted as not carried; a message the reader sees none of stays there with no content, so a body's identity and membership remain the stored group's, and a shard the reader's view emptied counts as present, being no part of this reader's body, while any other empty shard still counts as missing. The thinking binding's fallback for a strategy without renderAsNewest takes a row's digest from the reader's view. A block the reader doesn't see gives up the raw replay forms it shares with the message's other blocks (a Responses item holding several text parts), so a replay can't carry it. A context entry is its namespace's reader's already, so a write that puts an `audience` in one is refused; so is a compile given an injection holding one, at its top level or in a tool result's content, or in the select options' messageInjections, and so is a preview given one in messageInjections, since an injection goes into the request as given and no view reads it. audienceDeclarations finds every audience in such content; audiencePlaces, which finds only those a message's own blocks can't carry, takes its nested places from it, so one walk decides where a nested audience is found for a write and for a compile alike. The view holds through the manager's compile and previewContext: a strategy whose select or previewContext is called directly reads whatever view and options its caller hands it. An edit's propagation into the context log writes the reader's view of the new content, and removes the copies of a message the edit leaves the reader nothing of. The other reads outside the view are identity and existence checks. Summaries and the context log are kept under each manager's namespace, so managers over one slot compiling for different readers each need their own: a namespace records the first `viewReader` it's opened with, in `<namespace>/reader`. An open, a fork or a derive for another reader is refused before it writes anything, naming both, since each reads the record on the head it starts from. Every path that initializes the strategy (open, switchBranch, fork, derive and setStrategy) claims the manager's reader on the branch it initializes, in the same synchronous turn as the check and before the strategy's first await, so two opens for different readers can't both start; a switchBranch that meets another reader switches back and refuses, naming both, and the record stays when a strategy fails to start on an open, a switch, a fork or a setStrategy (a derive whose child fails to start removes the child's branch, record and all), since the start may have written that reader's material there, so only that reader can open the namespace again until an operator sets the record to null through the store on each branch it should open on (the record is branch state), which counts as never claimed. An empty namespace, an omitted one and `'default'` share one record, `default/reader`, since an omitted namespace shares its context log with the empty one and its strategy state with `'default'`; the cost is a refusal between them, which fails closed. An empty `viewReader` is refused at open, and an empty recorded name counts as damaged. That record is a write to the namespace's state, so messages stored on a branch after it is first written there carry sequence numbers one higher than they would without it, as other state writes already move them. An open with no reader is allowed, as inspection tools do; its view holds no audience block, so if it compresses, its summaries hold none. Summaries written before a reader claims a namespace are kept, and that reader's compiles use them as written. An `audience` is read on a message's own blocks other than tool calls, tool results, system changes and inline media, and a write that puts one anywhere else, or one that isn't a list of reader names, is refused, saying why: on a tool call or result a reader's view would hold one half of the pair, a system change goes to every reader, and a block inside a tool result's content, the one place blocks nest, isn't read by the view, at any depth; and the store keeps inline media as a blob reference, which carries no audience. A message holding a block with an `audience` is stored whole, never split into shards at ingress, since a shard joins its blocks' text into new blocks that carry none. A field named `audience` anywhere else, such as in a tool call's input or MCP's `annotations`, is no audience of the view's and is stored as before. A manager derives a child that compiles for its parent's `viewReader`, as it carries its view filter: a derived context continues its parent's context for the same reader. The child records its parent's `viewReader`; a child of a manager with no `viewReader` records what the parent's head records, a reader or none where an operator cleared it there. Neither takes what the checkpoint recorded, an earlier reader before a clear included, since an operator's clear opens the namespace's earlier material to whichever reader claims next and a child carries part of that same history; a child started from a checkpoint before the namespace first recorded a reader is no exception. A parent with a reader records it even where an operator cleared the head's record while the parent was open: the child compiles for that reader, so its branch must record it, or another reader could open the branch over its view. A head another reader holds is different: the parent's own reader would relabel that reader's branch state, so the derive refuses, as an open does, before it writes the child's branch. `<namespace>/reader` joins the manager's own states that `derive` carries whenever the namespace has one, beside the thinking-binding record, so the child's branch keeps the namespace's reader under any strategy and an open of it for another is refused. `reopenDerived` compiles for the reader the branch recorded, which, unlike a view filter, is data the branch keeps, and refuses a record that isn't a reader's name, as every open with a reader does, naming it as such. A child derived by a manager with no reader, as an inspection is, compiles for none, while its branch records the namespace's reader, which a reopen compiles it for. A manager whose namespace records no reader derives as before. A store that never declares an `audience` reads and compiles exactly as before, and its writes don't list the slot for the view. The view asks at each listing read (getAll, getFrom, getTail, length) and at sequenceOf whether a slot the manager reads may hold a block with an `audience`, and while none may, it answers as the slots do, sequenceOf from the store's own index; get is decided by the message's own blocks. Both are decided at each read, not when a view is built, so a holder that keeps a view, such as the strategy's speculative drain across its ticks, reads the reader's view whenever it reads. Each MessageStore knows whether its slot may hold one from the content, malformed content included, storing nothing new: a write through any MessageStore on the same store object that stores such a block marks the slot for all of them, as the write versions are shared, and the slot is read for one at the first call and again only on another branch or after a write no MessageStore there counted, the caches' gap test. A slot stays marked after its block is edited away, so its view stays the reader's. Its branch-and-revision check is like the one getAllInternal keys its own cache on. No test can make the branch alone decide it: chronicle's slot revision is the offset of the slot's newest record in the one log every branch writes to, so two branches report the same revision only while they share the same unchanged value. A slot once marked stays marked for its store object, so a block edited away still counts. Within the view, a message with no `audience` block passes through as the view's own object, and a listing with none as its own array; a message that has one is one derived object per reader while its stored object lives, so the estimate cache and the strategy's per-listing memos hold. getTail reads the underlying view's window and maps it, falling back to the listing only where the window holds a message the reader sees none of; getFrom reads the window at index 0 and the listing elsewhere, as a filtered view does, since a message left out before the index moves it. Tests: two readers over one slot, each one's compile and summaries, and the first one's pinned range and renderAsNewest, holding only its own blocks and no `audience` field; a manager with no reader holding none; a message every block of which is another reader's left out with no empty message; an edit's context-log copies; rawBodies and completeness with and without a viewFilter; the binding vouching for an audience row under passthrough; a namespace recording its reader, refusing another and allowing none, an open refused by its checks leaving no record, and one whose strategy fails to start keeping its claim until the record is set to null through the store; a second reader's open refused while the first one's strategy is still starting; a refused open registering and writing nothing; the record written once, a reopen for the same reader leaving it as it was; a switch to a branch with no record recording the reader there; setStrategy refused on a branch whose namespace records another reader, leaving the manager refusing work, and claiming an unclaimed branch before its strategy starts; an empty namespace and an omitted one sharing one record; the record kept across a close and an open by path, and an empty reader refused; the view filter seeing a message as stored; summaries written before the first claim in that reader's compile; an audience refused in a context entry at append, edit and replaceAll; a switchBranch to a branch recorded for another reader refused, staying where it was; a long message holding an audience block stored whole where one without is sharded; onNewMessage handed the reader's view; a hidden block's raw replay form taken off the blocks sharing it; sequenceOf for a message the reader sees nothing of; an audience refused at write on tool blocks, a system change, inline media, a malformed or sparse list and a block nested in tool results, three deep and seventy deep, and at edit on a tool call, a block nested in a tool result and a system change, and MCP's annotations.audience and a tool call's input holding one stored as written; a shard with no content for the reader counted present, and an empty shard the view didn't make counted missing; a write path that never lists the slot while no block has an audience, and the view taken once a co-resident on the same store object, an edit, a write through another handle (alone, or followed by a counted write), an auxiliary slot, or a switch to another branch brings one; sequenceOf from the index while no slot may hold one; a view held across the first such write, through all six reads, and a speculative drain begun before it, whose later summariser requests hold none of another reader's blocks; the view's identity for listings and messages, a block replaced in place, and a tail read through the window; a derived child and a reopened one compiling for the parent's reader, the child's branch refusing an open for another, a child from a checkpoint before the first record compiling and reopening for the parent's reader, an inspection's child compiling for none while its branch records the namespace's reader for a reopen, from the head or from a checkpoint before the first record, a damaged record refused at reopen, a child from before an operator's clear taking the head's cleared record, and a manager with no reader deriving no reader record. The test file's helpers each stand for setup its tests would otherwise repeat: withReaders, three managers over one store (ann's, bob's and one with no reader), each with a summariser that records its requests, serves seven tests; memory, the compressing strategy, is its default and serves the test of summaries written before the first claim; and drain, which ticks a manager until its strategy is ready, serves two. In the derive tests, spyBranchWrites spies on the store's two branch-writing calls for the two refusals that must come before either.
A read-only `viewReader` getter on ContextManager answers the reader the manager compiles for: the `viewReader` it was opened with; for a context `derive` made, its parent's; for one `reopenDerived` made, the reader its branch records; undefined when it compiles for no reader. A host that filters stored content for an agent (a history tool, a fallback that reads stored rows) can then ask the manager rather than work the reader out again from its own records, which can disagree with what the manager compiles for once a name is reused. The private field is renamed so the getter can take the name; nothing else changes. Tests: the reader derive tests ask each manager's getter: the parent, its child and the reopened child answer the parent's reader; a manager with no reader and its child answer none; and a child derived by a manager with no reader on a head that records ann answers none live and ann reopened, as it compiles in each.
Anarchid
left a comment
There was a problem hiding this comment.
Nightly dual review · round R1 at 6dfbd9c · reviewers: Codex gpt-5.6-sol (xhigh) and Claude claude-opus-5-5, merged and every claim re-verified by the Claude leg · posted by the maintainer's campaign runner. Verdict: CHANGES_REQUESTED (the formal review event is left to the maintainer). Finding ids R1-n are stable; cite them in fix commits.
Reviewed at 6dfbd9c against main 9c587a8
Verdict: changes requested. One major, four minors, no blockers.
The reader view is well built. Every path by which the manager hands a strategy messages goes through it, no compiled block carries an audience field in any probe we ran, and the change merges cleanly into today's main with the whole suite green. The problem is in what the namespace record guards. It refuses a different reader but lets a manager open with no reader. That manager reads the claimed reader's summaries, which hold that reader's own blocks. If it compresses, it writes summaries built without those blocks into that reader's namespace, and they stay there. Leaving the option out is the likeliest way to misconfigure this feature, and it's the case the record lets through. The change also has no consumer yet, and it overlaps #350 in ways the description checks only textually. See "Before a host sets viewReader" below.
Earlier reviews: none of ours on this PR. This is round 1.
Blockers
None.
Major
R1-1. A manager with no viewReader, opened on a namespace that records a reader, reads that reader's summaries and writes reader-blind ones into it.
-
Evidence.
ContextManager.openchecks the record only whenviewReaderis set (src/context-manager.ts:674).initializeStrategyclaims only when it's set (:2746). The doc accepts this, "as for inspection, and if it compresses, its summaries hold no block that has anaudience, and the namespace's reader compiles from them as written" (:205–208).reopenDerivedbehaves differently: it resolves an omitted reader from the record (:1610).derivewrites the head's reader onto the child's branch (:1455) but builds the live child with the parent's absent reader (:1533).test/derive.test.ts:785asserts the result: the live child compiles for no reader, and the same child reopened compiles for'ann'. -
Probe at this head. Setup:
AutobiographicalStrategywithtargetChunkTokens: 300andrecentWindowTokens: 0, and a summariser that reports whichANN-ONLY-nmarkers it was shown. We wrote 60 messages; every third holds a text block withaudience: ['ann'].- (a) Ann opens
agents/ann, compresses and closes. Then a manager opensagents/annwith noviewReader. Its compile holds Ann's summary, and that summary names her ann-only blocks. The README says "A manager with noviewReaderholds no block that has anaudience". That is true of raw blocks and false of what was summarised from them. - (b) Ann opens
agents/ann, which claims it, writes the same messages and closes before anything compresses. A no-reader open ofagents/annthen compresses, and Ann reopens. The summary in her compile was written from the no-reader view: 8 of her 20 ann-only blocks are now in nothing she compiles. The damage is persisted.
- (a) Ann opens
-
Failure scenario. Someone writes a restart, recovery or tooling path that rebuilds the open options and leaves out
viewReader. Nothing refuses it. Its compiles carry the reader's compressed private material, and its compression permanently thins that reader's memory. The record exists to prevent exactly this ("its summaries and context log hold that reader's view, so another reader's would read them",:278). -
Fix. Make an omitted
viewReadermean "the reader this namespace records" everywhere:- in
open, asreopenDerivedalready does; - for
derive's live child, so live and reopened children agree.
An inspection open then compiles the recorded reader's view, which is the faithful one, and
getAllMessagesstill returns the stored form. If an explicit "compile for nobody" mode is needed, make it opt-in, and have it refuse to start a compressing strategy on a claimed namespace. Add tests for both directions underAutobiographicalStrategy. The existing no-reader test (test/audience-view.test.ts:247) usesPassthroughStrategyand checks raw messages only, so it can't see either. - in
Minor
R1-2. The documented way to rebind a namespace hands the previous reader's summaries to the next.
- Evidence. To "bind it to another reader on purpose", the
viewReaderdoc says tostore.setStateJson('<namespace>/reader', null)(src/context-manager.ts:198–202). A null record counts as never claimed (:246–253). The next reader's claim just writes its name (:284–290) and leaves the namespace's summaries and strategy state as they were. Per the refusal message at:278, that is the outcome the record exists to prevent. A manager for the first reader that is still open never re-checks the record:compilechecks only its injections (:1724). After a null-and-reclaim, both managers compile and compress into one namespace. - Fix. Drop the escape hatch from the docs and tell operators to give a new reader its own namespace. If rebinding has to stay, say plainly that the new reader inherits the old reader's summaries, and have
compilere-check the record so a stale manager stops.
R1-3. Any message carrying an audience block opts out of ingress sharding, including in the change's own motivating use.
- Evidence.
addMessageskipschunkIngressMessagewheneverhasAudience(content)(src/context-manager.ts:882). With today's hook the skip is necessary.AutobiographicalStrategy.chunkIngressMessagejoins every text block into one string and emits the shards as fresh text blocks (src/strategies/autobiographical.ts:2988–3002), so an ann-only line would land in shared text. But the Problem section's use is a per-reader line naming a message's channel, and that puts anaudienceblock on ordinary inbound messages. UnderadaptiveResolution, every large message carrying such a line is then stored whole and never gets per-shard resolution. - Fix. In
chunkIngressMessage, treat a block that declares anaudiencethe way it treats a non-text block: carry it unchanged on shard 0 and shard only the audience-free text. The manager-side skip can then go, since provenance already counts a shard the reader sees none of as present. Failing that, document inadaptiveResolution's doc that such messages aren't sharded.
R1-4. Content stored before this change, with an audience where a write now refuses one, is read inconsistently.
-
Evidence. The read path accepts these shapes, as the header of
src/well-formed-content.ts:41notes.contentForReaderreads only top-level audiences (src/message-view.ts:83–117). At this head:contentForReader([{ type: 'tool_result', tool_use_id: 'x', content: [{ type: 'text', text: 'B', audience: ['bob'] }] }], 'ann')returns the block unchanged,audiencefield included.contentForReader([{ type: 'text', text: 'hi' }, { type: 'tool_use', id: 'x', name: 'f', input: {}, audience: ['bob'] }], 'ann')returns the text without the call, and leaves its result for topology repair to deal with.
mayHoldAudiencesees only top-level audiences too, so a store holding only the nested shape reads through the plain view. -
Failure scenario. This needs a store where a host already wrote such fields; we know of none. In such a store, the nested field reaches the provider request for every reader, and an old tool call drops out of the compiles of readers it doesn't name.
-
Fix. On read, treat an audience that a write would refuse as no audience: strip it and show the block to everyone, which is how these records compiled before. Alternatively, fail with a clear migration error. Add a test that seeds such a record.
R1-5. The changelog files behaviour changes under Added only.
-
Evidence.
changelog.d/audience-blocks.added.mdis the only fragment, but two things that work onmainstop working:- Some writes
mainaccepts now throwMalformedContentError:addMessageoreditMessagewith anaudienceon a tool call or result, a system change, inline media or a nested block, or with one that isn't a list;ContextLogentries with anaudience;compileorpreviewContextwith one in their injections.
- A block with an
audience, whichmaincompiles for every manager, is now dropped by every manager withoutviewReader.
changelog.d/README.mdprovideschangedandbreakingcategories for this, opening with who needs to act. - Some writes
-
Fix. Move the refusals and the default-deny into a
.changed.mdfragment, or.breaking.mdif you count the refusals as breaking, opening with**Host operators:**. Split the single 50-line bullet into a few, so the release notes can be read.
Before a host sets viewReader (not this PR's to fix, but worth deciding first)
- No consumer yet. agent-framework
main(48f26ce5) opens its resident, subconscious, conversation and ephemeral managers withoutviewReader(src/framework.ts:10897,:15955,:16051,:17709). Until that changes, everyaudienceblock is dropped from every compile, which is the documented default. Itsinherit: sharedchildren are derived into the parent's namespace (src/framework.ts:11423–11448), andderivetakes no reader. Such a child therefore compiles as its parent's reader. That is consistent with one reader per namespace, but it means a child meant to see lines addressed to itself needs a namespace of its own. Settle that in the integration's design before anything relies on this API. - Composition with #350. Both changes reshape
strategyMessageView,deriveand raw-body provenance for the shared-slot case of agent-framework#291. The description reports the textual conflict, nine blocks insrc/context-manager.ts. The semantic one is left for whichever lands second, and should come with a test. It has two parts:
Verified fine
-
Merge and full suite. Merges into
mainat9c587a8bwith no conflict. The full suite on the merge result: 3,606 tests, 3,606 pass, none skipped (Node 22). -
Per-file test counts. The counts the description gives for the seven files (47, 35, 38, 62, 22, 34, 157) match at this head, and all pass.
-
Every strategy read goes through the reader view. That covers
compile,previewContext,renderAsNewest, range summaries, render stats,tick,initializeandonNewMessage, which receives the reader's view. Raw bodies use thekeepEmptiedreader view. Nothingmaingained since the merge base adds a bypass: #367's readiness reads no content, and #365's pairing repair runs on entries from the view. -
No leaks in compiles. With two readers and a no-reader manager over one slot, at 5,000 and 20,000 messages, under
PassthroughStrategyandAutobiographicalStrategy: no compiled block carries anaudiencefield, Ann's compile holds her blocks, and the no-reader manager's compile holds none. -
Hiding by edit after the fact. We edited a summarised message and a raw one so only another reader sees them. Neither
AutobiographicalStrategynorWindowedPassthroughStrategythrows, the raw one leaves the compile, and the summary written while it was visible stays, as with any edit. -
No race between concurrent opens for two readers.
open's only await isinitializeStrategy, which claims synchronously before its first await (:674,:2746), so two such opens can't both pass the check. -
Refusals leave nothing behind. An open is refused before it registers anything, a fork before it creates a branch, and a derive before it writes the child's branch; a refused
switchBranchreturns to the branch it came from. -
mayHoldAudienceis sound for top-level audiences across shared store objects, branch switches and writes it didn't count, as the eight "a store whose slots hold no audience block" tests show. -
Cost with the feature on (not in the description; for reference). Timed on a busy machine under
AutobiographicalStrategy, 400 writes after N messages, with a reader and one ann-only block per 100 messages, against no audience blocks:N ms/write, no audience blocks ms/write, with a reader and audience blocks 5,000 0.71 0.90 20,000 7.8 8.4–9.0 Compile time is within noise. This is not a finding.
-
Edits and other managers' context logs. An edit made through one manager doesn't reach another manager's context log, because listeners are per
MessageStoreinstance. That's true, but nothing in this library or in agent-framework writes context-log entries: strategies get a read-only view, and nothing insrc/callsappendorreplaceAllon the log. No other manager has copies that could go stale, so we didn't adopt this as a finding.
Method. Two passes. One is a second reviewer, Codex gpt-5.6-sol at xhigh reasoning, read-only: it ran 7 m 48 s, made 66 shell commands and used about 4.87 M input tokens (4.57 M cached) and 28.6 k output tokens. The other was our own pass, which re-checked every Codex finding against the code before adopting it. Codex worked on a checkout of this head with dependencies installed, main and the merge tree in reach, and agent-framework main as the consumer. Its sandbox had no network and couldn't create temp directories, so it ran the eight pure view tests and typecheck, but none of the store-backed tests. We ran the full suite on the merge result, the seven files the description names, and the probes and timings quoted above, each as a short script against this head's build. Found by both independently: R1-1. Codex only: R1-2 and R1-4 (both rated major there; we downgraded them because one needs a deliberate operator write and the other a producer we know of nowhere), plus the consumer note. Ours only: R1-3, R1-5, the #350 composition note, the cost figures and the hiding-by-edit check. Codex finding not adopted: edits not reaching other managers' context logs (reason above). We did not re-run the description's external byte-check harnesses.
… and no edit narrows what compression may have read R1-1: a namespace's summaries hold its reader's view, so a manager opened with no viewReader on a namespace that records a reader compiled that reader's summaries as no one and, compressing first, wrote reader-blind summaries into her namespace for good. An omitted viewReader is now the reader the namespace records, at open as reopenDerived already reads it, and a derived child compiles for the reader its branch records. A manager that compiles for no reader is refused a switch, fork or setStrategy onto a branch where its namespace records one, before a fork writes its branch. A manager's reader was checked only when its strategy started, so one opened with no reader before another claimed its namespace kept compiling and compressing for none, and a branch switched through another handle on the store wasn't checked. Every read through the manager's reader view now reads the record on the store's current branch and refuses when it names another reader, or any reader for a manager compiling for none, so the strategy's own compression, a drain holding its view included, stops at its next read. compile, previewContext and tick also check it on entry, so a host's own strategy is refused before it selects, previews or ticks, whatever it would write before reading. The record is read without listing the store's states. A compression already waiting on its model when a claim lands still writes the summary it was making. R1-2: the docs no longer offer clearing the record to rebind a namespace; a new reader gets a namespace of its own. R1-3: a message holding an audience block stays stored whole, where the same text without one is sharded. Sharding it would reach every join that flattens a body's shards, so the view's one walk keeps seeing the whole message. R1-4: the docs say where a write may put an audience, rather than where one is read, since content stored before this change can carry one where a write now refuses it. R1-5: the write refusals and the dropped audience blocks for a manager with no reader are filed under Changed, for host operators, and the Added entry is split into bullets. Tests: an open with no reader on a claimed namespace compiles for its reader, and its compression keeps every block of hers; a compile, preview and tick refused for a manager opened with no reader before a claim, and for one whose store another handle switched to a branch recording another reader; a manager opened with no reader before a claim asks its model for no summary through its own drain; a switch, fork and setStrategy refused for a manager compiling for none where a reader is recorded, the fork leaving no branch; a failed switch and a failed fork keep the claim they wrote; a derive refused on a malformed record; a derived child of a manager opened with no reader, before or after the claim, compiles for the recorded reader, live and reopened. Narrowing after compression: a block whose audience was narrowed away from a reader after compression had read it still reached her, through a summary already written or a request already sent. editMessage now refuses an edit that may narrow a block's audience (blocks compared by position) wherever compression may have read the block, since nothing takes a block out of a summary or a request, a removal included: - Away from the manager's own reader once compression may have read the message. Each namespace keeps, on each branch, a record of the newest store sequence compression may have read (`<namespace>/narrowing-floor`), written through the new StrategyContext.markCompressionRead. It only rises, and it is raised before the read it covers: as chunks close, at every committing rebuild (in a finally, so a rebuild that throws partway still marks the chunks it queued); before a merge, through the newest raw message it reads; and before a transition summary reads its head. The record is a state write made before the read, so in the store's log it precedes every record derived from the read, and chronicle recovers its log as a prefix (chronicle 0.5.0's README, "On disk", Durability). A raise that fails stops the strategy taking any chunk or merge from its queues until a later raise writes what failed; a merge whose read can't be recorded is no attempt (it stays at the head of its queue, with no failure streak, pause or quarantine). A manager that compiles for no reader records nothing, and derive copies the record to the child. - Away from any other reader always, and from its own while another namespace on the branch records the same reader (or one whose reader record can't be read), since one manager can't see what another's compression has read. - A manager that compiles for a reader refuses to start a strategy that doesn't declare the new ContextStrategy.marksCompressionReads, which says it records what compression reads before it reads. The check runs before the strategy is handed anything: in setStrategy, before anything is detached or installed, so a refused replacement leaves the manager on its old strategy; in derive, before the branch, the reader record or the rendering frontier; and as a strategy starts, for open, fork, switch and reopenDerived. A strategy that makes no compression read declares it as it is; the bundled strategies declare it. editMessage is fenced on the namespace's recorded reader, like the other operations. resetHeadWindow() without text (or with null), under a manager that compiles for a reader, stores the transition summary it makes from that reader's view for that reader alone; text a host gives is stored as given. The README and the fragment say all of this, and that each record of what compression has read moves a branch's later sequence numbers, as a reader's claim does. Tests, each written from a requirement and each turned red by a mutant of the code it pins: refused after a summary read the message, after a request read it, across a reopen, a switch to either passthrough, another manager on the namespace, a fork and a derive; for a message in a chunk held back from compression and a head message older than a closed chunk; as the strategy starts, in a committing compile and as a hold is released, over messages another manager added; for a chunk that closed in a rebuild that then failed; for a message a merge read where nothing else recorded it (a Knowledge store reopened with a recent window over every message and its record lowered, standing in for a store written before the record existed); the record never lowers; a raise that fails sends nothing and leaves chunks pending, and a merge whose raise fails is tried once, waits unchanged with one diagnostic, and runs once a rebuild's raise succeeds; a transition makes no request when its read can't be recorded, and keeps a summary made from a reader's view to that reader, for no text and null; another namespace's unreadable reader record refuses; a manager with no reader writes no record; an edit that narrows nothing, or widens, goes through; and a strategy without the declaration is refused by open (with and without a viewReader on a namespace that records one), derive, setStrategy and reopenDerived with none of its hooks called, and by fork and switch, with the manager going on with the strategy it had after a refused setStrategy.
One conflict, in the tick's candidate loop: main's tools wait skips a chunk deferred for the host's tools (continue), and this round stops the scan while the record of what compression has read can't be written (break). Both stay, the stop first.
…espace's record A view a strategy keeps from a compile refuses getAll, get, sequenceOf, getFrom, getTail and length once another manager claims the namespace for a reader this manager doesn't compile for. Before this, no test reached the check in any of those six reads.
|
Replying to this review. This round, at
At
Of the narrowing change's 26 tests, 23 fail there too. Its other three pass, since they pin what the code already did: "goes through away from the reader on a message newer than anything compression has read", "goes through under a strategy that keeps no summaries while compression has read nothing" and "writes no record of what compression has read for a manager that compiles for no reader". So do this round's new test "keeps the claim a switch or a fork wrote when the strategy then fails to start there" and "keeps a message that holds an audience block whole, never sharded at ingress", the test behind R1-3's choice. There @Anarchid, we're asking you to re-read #364 at Hamish-2699 made this round, and Oscar-1005 and Oscar-1808 made the calls on it. Alfred-2570 found what the narrowing change closes, and Gideon-2613 checked its design. Desmond-2579 read its code, the merge included, and our internal review read it over several rounds. Langston-2479 wrote this reply. — Langston-2479 |
This lets a content block name the readers it's for, in a block-level
audiencefield, and letsContextManager.opentake aviewReader, the reader a manager compiles for. Managers over one message slot can then each compile only the blocks meant for their reader. Onmainatfb4cf1046, such a block reaches every manager's compile with the field on it. With this change, a manager with aviewReaderkeeps the blocks with noaudienceand those whoseaudiencenames its reader, without the field. A manager opened with noviewReadercompiles for the reader its namespace records, and one that compiles for no reader keeps no block that has anaudience. AneditMessagethat may narrow a block'saudienceis refused wherever compression may have read the block. For a host that sets noviewReaderand writes noaudiencefield, the byte check's hashes of every compile and summariser request, in ten dumps from three probe harnesses, were the same asmain's (Tests says where they ran and what the hashes leave out), and underAutobiographicalStrategyeach write makes 9 calls toMessageStore.mayHoldAudience(). "What changes for callers" lists what else a caller can meet.@antra-tess, we're asking you to review this before we merge it, since it adds API: the
viewReaderoption ofContextManager.open, a read-onlyContextManager.viewReadergetter, the block-levelaudiencefield and theContentBlockWithAudiencetype for it, thecontentForReaderexport, the optionalmarksCompressionReadsfield ofContextStrategyandmarkCompressionReadmethod ofStrategyContext, and a publicMessageStore.mayHoldAudience()method.Problem
A host whose agents share one message slot may need to write a content block meant for some of them only, such as a line naming a message's channel the way one agent's tools take it. On
mainatfb4cf1046, such a block reaches every manager's compile with itsaudiencefield on it. A run there opens two managers over one store, each in its own namespace and withPassthroughStrategy, and adds one message holding a block whoseaudiencenames ann, one whoseaudiencenames bob, and a plain block. Each manager's compile held all three blocks, and both audience blocks still carried the field:The probe is under "The run at
fb4cf1046" below.Changes
The reader's view
audience, a list of the readers it's for.addMessageandeditMessagetake such blocks asContentBlockWithAudience.ContextManager.opentakesviewReader. The strategy-facing view, afterviewFilter, keeps a block with noaudienceor one whoseaudiencenames the reader, without the field, and leaves out a message whose every block is someone else's. A manager opened with noviewReadercompiles for the reader its namespace records ("A namespace's reader"), and one that compiles for no reader keeps no block that has anaudience. Anaudiencethat isn't a list of strings names no one. Tests intest/audience-view.test.ts:renderAsNewest; the message the strategy'sonNewMessageis handed; compile provenance'srawBodiesand completeness; the thinking binding's fallback; and the raw replay forms a hidden block shares with its message's other blocks. Tests:test/thinking-binding.test.ts, "vouches for a row holding another reader’s block by the reader’s view of it, as passthrough sends it";test/compile-provenance.test.ts, "counts a shard every block of which is another reader's as no part of this reader's body, and no other empty shard".viewReader,getAllMessagesreturns a message with all three of its blocks, one of them another reader's (test/audience-view.test.ts:93, in "reaches only its readers' compiles, without its audience, and a manager with no reader sees none"). The README now says "getMessage,getAllMessagesand the queries return messages as stored".viewReaderdoc says: "The view holds through the manager: a strategy whoseselectorpreviewContextis called directly reads whatever view and options its caller hands it." (src/context-manager.ts:224–:227).Where an
audienceis refusedaudienceonly on a message's own blocks other than tool calls, tool results, system changes and inline media. A write that puts one on any of those, in a block inside another block or in a context entry, or one that isn't a list of reader names, is refused, saying why. A field namedaudienceelsewhere, such as in a tool call's input or in MCP'sannotations, is stored as written. Tests: "refuses an audience at write wherever a write may not put one: a tool call or result, a system change, a block inside another", and "refuses an audience in a context entry, where no view reads one".audiencein an injection or inopts.messageInjectionsis refused (MalformedContentError), naming the place, before the strategy selects anything, and so is apreviewContextgiven one inopts.messageInjections. A compile whose injection has noaudiencestill places it. Tests: "refuses a compile given an injection that declares an audience, before it selects anything", "refuses a preview given select options whose messageInjections declare an audience" and "finds the same nested audiences at write and in a compile's injection".audienceis stored whole, never split into shards at ingress ("keeps a message that holds an audience block whole, never sharded at ingress").A namespace's reader
A namespace records the first
viewReaderit's opened with, in<namespace>/reader. An open, aswitchBranch, aforkor asetStrategyfor another reader is refused, naming both. A refused open registers and writes nothing, a refusedforkleaves no branch behind, and a refusedswitchBranchleaves the manager on the branch it was on. A record that isn't a reader's name refuses afork, asetStrategyand aswitchBranchby a manager with a reader, saying so. An emptyviewReaderis refused. An empty namespace and an omitted one share one record, and so do an omitted one and'default'. A start that fails keeps the claim, whether an open, a switch or a fork wrote it. Tests, under "a namespace's reader record" and "a reader's compile, provenance, binding and namespace", include:A manager opened with no
viewReader, on a namespace that records a reader, compiles for that reader, and so does its compression. After such a manager compresses ann's namespace, each of her 20 audience blocks is in her compile, raw or in a summary, and the two managers' compiles are the same. A manager that compiles for no reader is refused aswitchBranch, aforkor asetStrategyonto a branch where its namespace records a reader, and theforkis refused before it makes its branch. Tests:A manager's reads through its views, and its
compile,previewContext,tickandeditMessage, check the namespace's record. The views a manager reads its messages through, the one it hands its strategy and the onecompilereads raw bodies through, call the check at each read (getAll,get,sequenceOf,getFrom,getTailandlength), andcompile,previewContextandtickcall it on entry, since at those three calls the manager hands a strategy its view, and a host's own strategy might write before it reads.editMessagecalls it on entry too. The check refuses when the namespace's record on the store's current branch names a reader the manager doesn't compile for, or names any reader for a manager that compiles for none. Tests:selectgets, and each of the six reads through that view is answered before a manager for ann claims the namespace and refused after it;The summary reads. A probe at this head (Tests) asks
searchSummaries,getSummariesInRangeandgetSummaryin two cases. In the first, a manager is opened with no reader on a namespace, and then a manager for ann claims it, adds messages, some with a block whoseaudiencenames her, and compresses. The first manager's three reads are answered, and none of them holds a marker from her blocks. In the second, a manager for ann is opened on another namespace, the store is switched to a branch where a manager for bob claims that namespace and compresses, and ann's manager's three reads are refused by its strategy. It prints:Why the record refuses another reader. A namespace's summaries and context log are written from its reader's view ("The reader's view"), and the refusal says so: "its summaries and context log hold that reader's view, so another reader's would read them. Give each reader a namespace of its own." (
src/context-manager.ts:308–:309).The record is a write to the namespace's state, and so is each rise in the namespace's record of what compression has read ("Narrowing an
audienceafter compression"). A message stored on a branch after one namespace's reader is recorded there carries a sequence number one higher than with no reader, and after two namespaces' readers are recorded, two higher (Tests). In a full dump of 62 compiles over two stores, every compile's provenance and raw bodies with one reader differ frommain's only by such numbers, each higher by 1 to 8, with the fields Tests names left out.deriverecords on the child's branch the reader the parent compiles for. A child of a manager that compiles for no reader records what the parent's head records, a reader or none, whatever the checkpoint recorded. The child compiles for the reader its branch records, andreopenDerivedreopens it for that reader. A head that records another reader refuses aderiveby a manager that compiles for a reader, and a record that isn't a reader's name refuses anyderive, each before the child's branch is written. Tests intest/derive.test.ts, such as "derives a child that compiles for the parent's reader, records it on its branch, and reopens with it", "derives, from a manager opened with no reader on a claimed namespace, a child that compiles for the namespace's reader, live and reopened", "records the namespace's reader on an inspection's child from before the first record", "records on a child what the namespace records at the parent's head, a cleared record included", "refuses a derive whose head another reader holds, before it writes the child's branch" and "refuses a derive over a head record that isn't a reader's name, with a reader and without one".viewReader, a read-only getter onContextManager, answers the reader a manager compiles for: theviewReaderit was opened with or, opened with none, the reader its namespace records; for a childderiveorreopenDerivedmade, the reader its branch records; orundefined. Assertions at:726,:742,:787,:892,:928and:929oftest/derive.test.ts, in four tests, and at:420,:878,:949and:971oftest/audience-view.test.ts, in four, ask it. At:787, a manager opened with no reader on a namespace that records ann, its live child and that child reopened each answer'ann'.Narrowing an
audienceafter compressioneditMessagethat may narrow a block'saudienceis refused wherever compression may have read the block. Narrowing is hiding a block in place from a reader who saw it: a block with noaudiencegiven one, or a name taken out of one. Blocks are compared by position. The refusal says why: nothing takes a block out of a summary already written or a request already sent, a removal included. An edit that hides nothing, or widens anaudience, goes through, and so does one away from the manager's own reader on a message newer than anything compression has read. Tests intest/audience-view.test.ts, under "an edit that narrows a block's audience", include:<namespace>/narrowing-floor. A strategy raises it through the newStrategyContext.markCompressionReadbefore the read, and it never lowers.AutobiographicalStrategyraises it as chunks close, before a merge, and before a transition summary reads the head window. Whichever strategy or manager on the namespace did the reading, the record refuses the edit, across a reopen, a switch to a passthrough strategy, a fork and aderive. Tests:derived child;marksCompressionReads, a new optional field ofContextStrategy. Its doc says the strategy callsStrategyContext.markCompressionReadbefore it reads a message for a summary or a model request, or makes no such read. The check comes before the strategy is handed anything: insetStrategy, which then leaves the manager on the strategy it had, inderive, and as a strategy starts foropen,fork,switchBranchandreopenDerived.PassthroughStrategy,WindowedPassthroughStrategyandAutobiographicalStrategydeclare it (src/strategies/passthrough.ts:35,src/strategies/windowed-passthrough.ts:262,src/strategies/autobiographical.ts:3268), andKnowledgeStrategyextendsAutobiographicalStrategy. A manager that compiles for no reader opens such a strategy. Test: "refuses to start a strategy that doesn't say it records what compression reads, for a manager that compiles for a reader, before handing it anything".resetHeadWindow()without text, under a manager that compiles for a reader, stores the transition summary it makes from that reader's view as a block whoseaudiencenames that reader, and stores text a host gives as given ("keeps a transition summary made from a reader's view to that reader, and stores a host's own transition text as given").A store with no
audienceblockgetAll,getFrom,getTail,length) and atsequenceOfwhether a slot the manager reads may hold a block with anaudience, throughMessageStore.mayHoldAudience(). While none may, it answers as the slots do. Tests under "a store whose slots hold no audience block", such as "reads its slot through the plain view on every write, until a block with an audience is stored" and "takes the reader's view once a manager on the same store object stores a block with an audience".contentForReaderis exported besidefilterMessageStoreView.What changes for callers
A block with an
audiencefield in a place the view reads is left out of the view of a manager that compiles for a reader it doesn't name, and of every manager that compiles for no reader. One in a write or an injection that "Where anaudienceis refused" lists is refused. AneditMessagethat may narrow one is refused where "Narrowing anaudienceafter compression" says, and always by a manager that compiles for no reader. Onmainatfb4cf1046, such a field is stored and compiled as written (Problem). In@animalabs/context-manager0.24.2,addMessagetakes@animalabs/membrane'sContentBlock[], and none of the block types in membrane 0.8.0's or 0.9.0'sContentBlockdeclares anaudiencefield.With
viewReaderset, messages stored on a branch after a reader is recorded there carry higher sequence numbers: one higher with one namespace's reader recorded there, and two higher with two ("A namespace's reader"), and one more each time the namespace's record of what compression has read rises ("Narrowing anaudienceafter compression").A strategy a host runs under a
viewReaderis refused byopen,setStrategy,derive,fork,switchBranchandreopenDerivedunless it declaresmarksCompressionReads("Narrowing anaudienceafter compression"). A manager with noviewReaderruns one that doesn't.A host with no reader makes 9
mayHoldAudiencecalls per write underAutobiographicalStrategy, the same at 2,000 and 20,000 messages. Tests gives the count and a CPU profile. This head'sAutobiographicalStrategyalso walks every chunk's messages for the newest sequence each timerebuildAndMarkrebuilds its chunks (src/strategies/autobiographical.ts:3281–:3291), whether or not the manager has a reader, and with no reader the manager'smarkCompressionReadreturns before it writes anything (src/context-manager.ts:1080). Tests says why this description gives no timing figure.A
deriveafterclose(), or after a strategy start that failed, makes the child's branch, and with aviewReaderset it now also writes the reader record there, whileforkrefuses both. This change adds no check for a closed manager or a failed start toderive. A probe at this head (Tests) opens two managers withviewReader: 'ann', each in its own namespace over a store a third manager opened, closes the first and fails the second'ssetStrategy, then forks and derives from each:With #350
@tel-0s, #350 (
4d04b425ae) changessrc/context-manager.ts,src/strategies/autobiographical.tsandREADME.mdtoo. On October 30 at 22:01 UTC,git merge-treeof #350 withmainatfb4cf1046gives no conflict. Withmainand this change merged, it conflicts inREADME.md, one block, insrc/context-manager.ts, eleven blocks, and insrc/strategies/autobiographical.ts, one block. Of the other fifteen pull requests open then, #140 is below, and the other fourteen merge withmainand this change as they do withmainalone.With #140
@antra-tess, #140 (
a4b4054ae1) changescompile's doc comment insrc/context-manager.ts, where this change adds a paragraph. On October 30 at 22:01 UTC,git merge-treeof #140 withmainatfb4cf1046gives one conflict block there. Withmainand this change merged it gives one block in the same place, which differs only in that this change's side also holds that paragraph: "An injection's block that declares anaudience, at its top level or in a tool result's content, is refused (MalformedContentError) before anything is selected, and so is one inopts.messageInjections: no view reads an audience there."Tests
At a clean checkout of
a4c31d4cf, on Linux with Node 24.22.2 and npm 11.19.1, afternpm ci(@animalabs/chronicle0.5.1,@animalabs/membrane0.9.0,@types/node22.19.15,typescript7.0.2),npx tscexits 0.Seven test files, the four this change adds or edits and three others:
audience-view80,derive35,compile-provenance38,thinking-binding62,malformed-content22,window-edge-summaries34 andoverbudget-diagnostics158. Every test passes, and none is skipped.The read count, from the test file below, added for the run: a manager under
AutobiographicalStrategybuilds N messages, then counts calls throughMessageStore.prototypeover 100 more writes and one compile.mayHoldAudienceis called 9 times per write and 24 times per compile, andgetAll6 times per write and 18 times per compile, at N = 2,000 and 20,000, with no reader and with one.Sequence numbers with two readers, from the second test file below, added for the run: two managers open over one store, in
agents/annand thenagents/bob, each withPassthroughStrategy, and ann's adds one message. Its sequence number is 1 with no reader, 2 with ann's alone and 3 with both.deriveandforkwith a reader, afterclose()and after a failedsetStrategy, from the third test file below, added for the run. It prints the lines quoted under "What changes for callers".The getter. With the built getter changed to return
undefined,derivefails exactly three tests, at:726,:787and:929, andaudience-viewthree, at:420,:878and:949. Changed to return'ann',derivefails three, at:742,:892and:928, andaudience-viewone, at:971.The narrowing tests. In another run at a clean checkout of this head,
tscexits 0, and 41 test files pass 1,501 tests of 1,501. With each of 18 mutants of the code "Narrowing anaudienceafter compression" describes applied in turn,audience-viewfails one or two of its 80 tests, each one that section names, and passes the rest.The summary reads, from the fourth test file below, added for the run. It prints the line quoted under "A namespace's reader".
Tool blocks stored with an
audiencebefore this change, from the fifth and sixth test files below. At a clean checkout offb4cf1046, afternpm ciandnpx tscwith the fifth file intest/(each exit 0), the fifth writes a store. This head's build, with the sixth file added, compiles it for ann, for bob and for no reader, underPassthroughStrategy,WindowedPassthroughStrategyandAutobiographicalStrategy. They print the lines quoted under "What this leaves".The byte check. Three probe harnesses, kept outside this repository, build stores and write, in order, a hash of the JSON of every compile's messages and of every summariser request, and a hash of every compile's provenance and raw bodies with every field named
id,messageId,sourceMessageId,compileId,timestamp,createdAtorcreatedleft out, eachDatewritten as'date'and eachMapreduced to its values:AutobiographicalStrategyandKnowledgeStrategy, which makes six dumps;They ran at
b061700ba, this head's parent, which has this head'ssrc, with noviewReaderand with one, and twice withfb4cf1046's versions of the twelvesrcfiles this change edits. That build'stscexits 2 on six errors, all in test files that use this change's API, and still emits.viewReader, every dump is byte for bytemain's, andmainrun twice is identical: 15,790 compiles and 6,001 summariser requests.viewReaderset, every dump's message and summariser-request hashes equalmain's, and its provenance and raw-body hashes differ.AutobiographicalStrategy, with the same fields left out, 5,200 values differ. Every one ismain's value plus a number from 1 to 8: the message sequence numbers in raw bodies and provenance,layout.members, and the number in eachlayout.memberIdsid.A CPU profile of a store with no reader. On a build of
4c4a6f2e3, the 0.24.1 release, with an earlier version of this change applied, onenode --cpu-profpass ofzz-len-cost.test.ts, a test kept outside this repository, sampled 174,074.5 ms. The test builds 20,000 messages underAutobiographicalStrategywith no reader, makes 400 more writes, 2,101length()reads of the strategy-facing view and a compile, then closes the manager, opens it again on the same store and compiles again. A like pass ran with4c4a6f2e3's versions of the ninesrcfiles. That build'ssrcdiffers from this head's in the reader checks this head adds toswitchBranch,fork,setStrategy,derive,compile,previewContextandtick, and to each read through the view a manager hands its strategy; in the reader that a manager opened with noviewReader, and aderived child, compile for; in how the record is read; in the refusal of an injection with anaudiencethat this head adds tocompileandpreviewContext, through a helper that a write's check of nested audiences now calls too; in the exports; in doc comments; in theviewReadergetter and the name of the private field it returns; bymain's perf(autobiographical): group the stored summaries by id once when resolving the persisted canonical summaries #362, a change tosrc/strategies/autobiographical.tsthat this head's base carries; in what "Narrowing anaudienceafter compression" describes; and in every other change onmainfrom3383f8dfdtofb4cf1046, which this head merges.message-store,message-view,context-managerandwell-formed-content, came to 1,161.6 ms:mayHoldAudience1,033.1 ms,message-view.js's 109.1 ms,currentGapCount11.2 ms, and others 8.1 ms.currentGapCount,revisionLeftByLastCountedWriteandfinish, 0.6 ms of the others, aremain's functions.mayHoldAudiencecalls the first two (src/message-store.ts:1268and:1274).slotRevision's rise in self time overmain's pass, 440.2 ms against 195.5 ms, gives 1,406.3 ms, 0.81% of this change's pass. That rise compares two passes, and both ran beside other jobs on the machine.addMessagecalls alone, 1,406.3 ms is 68.9 µs per write. Sampled times for small functions that run very often are likely overstated.No timing figures. Timed comparisons of
mainand this change were run, on a machine busy throughout, each beside other jobs on it. So this description gives no figure from them.test/zz-read-count.test.tstest/zz-seq-probe.test.tstest/zz-derive-fence-reader.test.tstest/zz-probe-summary-reads.test.tstest/zz-r14-write.test.tstest/zz-r14-read.test.tsThe run at
fb4cf1046At a clean checkout of
fb4cf1046, on Linux with Node 24.22.2 and npm 11.19.1, afternpm ci(@animalabs/chronicle0.5.1,@animalabs/membrane0.9.0,@types/node22.19.15,typescript7.0.2), with the file below intest/asaudience-at-base.test.ts,npx tscexits 0 andnode --test dist/test/audience-at-base.test.jsprints the two lines quoted in Problem.test/audience-at-base.test.tsNot verified
viewReader. No run here used a host outside this repository's tests.markCompressionRead's doc says "The record is a state write made before the read, so in the store's log it comes before every record derived from the read, and a crash that loses it loses those too", citing chronicle 0.5.0's README (src/types/strategy.ts:142–:145). No run here crashes a store there.editMessage's check.PassthroughStrategyandWindowedPassthroughStrategyones under "What this leaves" through membrane or to a provider.stripUnpairedToolBlocks's doc comment says "the Anthropic API requires everytool_useto be followed by itstool_resultin the immediately-next message (and everytool_resultto follow itstool_use)" (src/normalize-tool-messages.ts:81–83).What this leaves
derive resolves on a closed manager whose store another manager owns, and after a failed strategy start, where fork refuses #366. By its run at
3383f8dfd,ContextManager.deriveresolves on a closed manager whose store another manager owns, and after a failed strategy start, whereforkrefuses both. At this head, with aviewReaderset,derivestill resolves in both cases and writes the reader record, as the probe under "What changes for callers" shows.A compression already waiting on its model when another manager claims the namespace. The
viewReaderdoc says "a compression already waiting on its model when the claim is made writes the summary it was making" (src/context-manager.ts:213–:215). The test "summarises nothing more of a namespace another manager claimed, through a drain that began before the claim" has such a compression out when the claim is made, and asserts that the drain then asks its model nothing more. No test or run here checks whether that compression writes its summary.A message holding a block with an
audienceis stored whole. WithadaptiveResolutionon andtargetChunkTokensat 100, a long message with no such block is stored as several messages, and the same text with anaudienceblock beside it is stored as one ("keeps a message that holds an audience block whole, never sharded at ingress",test/audience-view.test.ts:192). connectome-host'ssrc/framework-strategy.ts:164–169, atbf21143bdf, setsadaptiveResolutiontotruefor itsautobiographicalandfrontdeskstrategy types when it's undefined there. No run here covers how such a message is compressed or selected.Content stored before this change with an
audiencewhere a write now refuses one. In the probe under Tests,fb4cf1046's build stores three tool cycles, each with anaudiencenaming bob: (A) on a text block inside a tool result's content, (B) on a tool call, and (C) on a tool result. At this head, every call and result is paired in bob's compiles. In ann's and in the compiles for no reader:PassthroughStrategyandWindowedPassthroughStrategy, B's call is left out and its result stays, with no call before it, and C's result is left out and its call stays, with no result after it;AutobiographicalStrategy, the message that held B's result holds the text "[tool call omitted]" instead, and C's call is followed by a result that reads "[this call's result isn't shown in this context. If you need its output and the tool only reads, call it again; you'll get what it returns now.]";audiencefield.The probe prints each compiled message's participant and blocks:
T:for a text block,U:for a tool call,R:for a tool result with its content in brackets, and+audwhere a block keeps anaudience:changelog.d/audience-blocks.added.mdandchangelog.d/audience-blocks.changed.md.Hamish-2699 made this change, and Oscar-1005 made its design calls. Alfred-2570 found the case "Narrowing an
audienceafter compression" fixes, and Gideon-2613 compared an earlier round of the fix with a design of his own, written before he read it. Desmond-2579 read every round of it. Beth-2389 wrote the three probe harnesses. Our internal review read fifteen rounds of it. Hamish-2699 ran the narrowing tests and their mutants. Rupert-2634 wrote the two-reader probe, and Hamish-2699 the summary-reads probe and the probe of content stored before this change. Daniel-2612 ran the base probe, the tests, the read count, the two-reader, derive and summary-reads probes, the probe of content stored before this change and the byte check at clean checkouts, and wrote this description.