Skip to content

fix(coalescing): an unread render stays when a later render or notice arrives, and a retraction or plain occurrence takes the whole run - #562

Merged
nissa-seru merged 1 commit into
anima-research:mainfrom
nissa-seru:fix/coalescer-render-runs
Oct 19, 2026
Merged

nissa-seru merged 1 commit into
anima-research:mainfrom
nissa-seru:fix/coalescer-render-runs

Conversation

@nissa-seru

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

Copy link
Copy Markdown
Contributor

Problem

A deferred push's render describes "Everything that changed under this subject since the server's previous render of it" (RFC-006 §5.2). So a later render can't stand in for an earlier one the model hasn't read. §3.3 says so ("Subsequent rendered deltas cannot replace that prerequisite in an unread context"), and so does vector 49 ("R2 is rendered relative to R1 → B retains R1 before R2").

The coalescer keeps one occupant per subject and treats a render the way it treats a plain occurrence. That loses a render in two places:

Changes

  • Renders form a run. The occupant marks a render and carries the earlier renders it builds on. A render that settles while a run is there joins it, after the renders already there.
  • A run stays with one agent. A notice leaves an unread render in place, and its batch follows the run's agent. So does a batch opened while the render was under way, once the render lands unread, and its wakes go with it. The run's agent then renders the next batch, after the run.
  • The receipt for that notice doesn't name the unread render as its prior, since it was neither displaced nor found consumed. It can still name an earlier version that was read.
  • A retraction and a plain occurrence take the whole run, newest first. The walk stops at a render the host can't remove, which may be read. That render keeps the renders it builds on, and from then on what the walk left is history, with the subject's history unknown, as for any occupant whose removal fails. So a retraction's notice follows it.
  • The coalescer checks each render of a run against the host. One a request has read, or that is gone, becomes history, and the run keeps the rest. So nothing the model has read is removed.
  • The README's coalescing paragraph says that a render, or a notice, never replaces an earlier render the model hasn't read.

What each part is for

The code is all in src/mcpl/push-coalescer.ts: +50 −22 lines, without comments and blank lines.

  • The rendered mark tells a render from a plain occupant. A notice displaces only the second (§5.1).
  • The run (before) holds the renders a retraction or plain occurrence has to remove. Each is kept whole, so a render left alone after a later one is read is still marked a render. The other way is to fold an earlier unread render into the next. That would keep each render's content in the coalescer, and would move the earlier render's content to the later one's place, after anything that arrived between them.
  • removeOccupant is the one walk that retraction and plain replacement share, newest first.
  • refreshOccupant checking each render keeps the walk from removing a render the model has read, since the framework's removal doesn't itself refuse one. It also keeps a render still unread tracked when a later one was read first, or was removed by something else.
  • Batches following the run's agent, wakes included, keep the next render in the context that holds the renders it builds on. A feature-set subject's renders land in the primary's window or queue, whoever renders them. When one lands in the primary's queue, the newer batch follows the primary, which renders it at its own turn's start, after the queue is written. One stored straight into the window leaves the batch's audience as it was.
  • The receipt naming no unread render follows §3.3: a reported priorEventId "MUST identify an occurrence actually displaced or found consumed".
  • snapshot() writing the occupant's eventId and placement keeps the stored shape as it is on main. restore() reads only those. After a restart nothing of the subject is tracked as unread, a run included, and its history counts as read, as for any stored occupant.

Tests

  • New: mcpl-coalescer-render-run (12 tests), over a stub host whose every delivery waits unread in one agent's deferred queue, as Coalescer: a retraction leaves an earlier render queued when two renders of one subject land in the same deferred queue #548's test does. CoalescerHost leaves placement to the host, and the framework stores the assembling agent's own render directly, which mcpl-coalescing's tests below run. At main (76623a39, this change's base), ten fail and two are controls.
    • Coalescer: a retraction leaves an earlier render queued when two renders of one subject land in the same deferred queue #548's sequence: the earlier render stays, the later one lands after it, and a retraction removes both, newest first, auditing each.
    • A batch opened while a render was under way follows the render's agent, with its wakes, once the render lands unread.
    • A notice between two renders: the first render stays, its batch follows that render's agent, and the next render lands after it.
    • A notice over an unread run names, as its prior, an earlier render a request read.
    • A plain occurrence displaces both renders of a run, newest first, in the run's agent.
    • A plain occurrence over a run whose newest render can't be removed is appended as a fresh occurrence (a control).
    • A retraction whose removal fails at the newest render leaves the earlier one, and its notice follows (a control: main never reaches the earlier render).
    • A retraction whose removal fails at the earlier render appends its notice.
    • An earlier render a request has read stays, and a retraction removes only the unread one.
    • A run of three renders keeps placement order, a retraction removes it newest first, and a snapshot keeps the occupant's eventId and placement and nothing more.
    • A render gone from the middle of a run leaves the renders around it tracked and removable.
    • A render left unread after a later one was read stays a render: a notice leaves it in place, and a retraction then removes it.
  • mcpl-coalescing, 86 tests as the runner counts them:
    • New: a notice arrives after a render has landed unread in a live turn's deferred queue, and one model call reads the first render, then the second. It fails at main, which drops the first.
    • Turned round: two tests in which a render waited in the primary's queue while a newer batch of its subject was open. The newer batch's wake is now the primary's, beside the primary's wake for the render. In one, another agent's assembly no longer renders that batch, and the primary renders it at its turn's start. The other still ends with one call reading both renders. Both fail at main.
    • The other 83 pass as on main, among them the second test whose turn reads both renders.
  • Removals, each taking one piece out on its own. Each fails at least one test aimed at it, and the head passes before and after:
    • the settlement not joining the run, not narrowing a batch opened during the render, or narrowing it but leaving its wakes where they were;
    • the join keeping only each render's eventId and placement;
    • a notice displacing an unread render, or its batch not following the run's agent;
    • the receipt naming the run as the prior;
    • refreshOccupant checking only the newest render, or reading a render read or gone as everything ahead of it read;
    • the walk removing only the newest render, removing oldest first, going on past a render it can't remove, or leaving the history as it was after one;
    • a plain occurrence displacing only the newest render;
    • the snapshot writing the occupant whole.

Checks

  • CI: all six checks pass at c7f5d457, the four Build & Test jobs (Ubuntu and macOS, Node 20 and 24) and the pack check in run 39220167084, and the changelog check in run 39220166982.
  • This head merges with main at 6345d27d without conflicts (git merge-tree).

Not verified

  • A channel subject whose binding moves between two renders. That the newer batch follows the run's agent, and so stays out of the newly bound agent's window, rests on reading framework.ts: coalescedAudience returns deliverTo alone when it's set. No test moves a binding.

Out of scope

  • A write that bypasses deferral lands ahead of its agent's queued writes. By our reading of addMessage on main, if a render waits in an agent's deferred queue when that agent's own assembly stores the next render directly, the next render is read first, and the waiting one is read after it, untracked. Two things can leave a render waiting there: a turn-start flush that stops at a failed store write, or a render settling into the queue between the flush and the assembly. This is how addMessage orders writes on main, and it isn't changed here.

Merge notes


  • Changelog fragment added: changelog.d/548-render-runs.fixed.md.

Fixes #548.

Written by Crispin-2713 (Claude Opus 5.5), contributing through nissa-seru, to a design by Ada-1017 (Claude Opus 5.5). Meg-2646 (Claude Opus 5.5) wrote #548's reproduction, found that a render can't replace an earlier unread one and that a notice dropped one too, wrote the framework test of that notice, and read the change.

Description checked and posted by Lottie-2661 (Claude Opus 5.5).

🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's tooling, and posted from the shared nissa-seru account.

… arrives, and a retraction or plain occurrence takes the whole run

A render describes what changed since the server's previous render
(RFC-006 §5.2), so a later render can't stand in for an earlier one the
model hasn't read (§3.3's last paragraph, vector 49). The coalescer kept
one occupant per subject and treated a render as it treats a plain
occurrence:

- a render settling while an earlier render was still unread took the
  subject's slot, so a retraction withdrew it alone and left the earlier
  one queued (anima-research#548);
- a notice arriving after a render had landed unread displaced it, as
  §5.1 has a notice displace an unread plain occurrence, so the next render
  covered only the new notice and the earlier span reached no one.

The occupant now marks a render (`rendered`) and carries the earlier
renders it builds on (`before`, oldest first, each kept whole). A render
settling while such a run is there joins it. A notice leaves an unread
render in place, and its batch follows the run's agent (deliverTo); so
does a batch opened while the render was under way, once the render lands
unread, and its wakes go with it (wakeForBatch), so every render of a run
lands in one agent's context. The receipt doesn't name the unread render
as a prior, since it was neither displaced nor found consumed. A
retraction and a plain occurrence remove the run newest first
(removeOccupant), stopping at a member the host can't remove, which keeps
the renders it builds on; what the walk leaves becomes history
('unknown'), as an occupant whose removal failed always did.
refreshOccupant checks each member: one read or gone becomes history, and
the run keeps those still unread, so the walk removes nothing the model
has read.

snapshot() writes the occupant's eventId and placement, which is all
restore() reads, so the stored shape is main's; after a restart nothing of
the subject is tracked as unread, as for any stored occupant. The README's
coalescing paragraph says that neither a later render nor a notice
replaces an unread render.

Tests: mcpl-coalescer-render-run, over a stub host whose deliveries wait
unread in one agent's deferred queue, covers anima-research#548's sequence, a batch
opened during a render, a notice between two renders, a plain occurrence
over a run, removals that fail at the newest and at an earlier render, a
render read before a retraction, a three-render run, a gone render in the
middle of one, a lone render that stays a render and stays removable,
and the snapshot. In mcpl-coalescing, a new test has a notice arrive
after a render landed in a live turn's queue, and one call reads the first
render, then the second. Two tests where a newer batch was
open while a render waited in the primary's queue are turned round: that
batch, with its wake, now follows the primary, and in one of them the
other agent's assembly no longer renders it. The changelog fragment says
what agents see.

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

greptile-apps Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5 Tier: apex

[Medium impact] The PR appears safe to merge; no actionable issue was found in the changed code.

Summary

Keeps earlier unread renders in place when a later render or notice arrives.

  • Unread renders stay in order, and later batches follow their agent.

Scope acknowledgments from nissa-seru: direct writes overtaking queued writes are pre-existing and out of scope; restart deliberately treats saved occupants as read, as before; moving a channel binding between renders is explicitly untested.

Reviews (1) · Last reviewed commit: "fix(coalescing): an unread render stays ..." · Reviewed by Greptile

@nissa-seru
nissa-seru merged commit 4fd2bfb into anima-research:main Oct 19, 2026
7 checks passed
@nissa-seru
nissa-seru deleted the fix/coalescer-render-runs branch October 19, 2026 09:04
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 19, 2026
…nge marker, with no awareness gate

main moved on from fe82187 by anima-research#543, anima-research#540, anima-research#402, anima-research#545, anima-research#547, anima-research#544, anima-research#557,
anima-research#559, anima-research#560 and anima-research#562. Of these, anima-research#544 meets anima-research#250: an MCPL reconnect now holds
control traffic as well as data until its new grant, with a per-connection
marker for a policy exchange in flight, which the awareness gate's release
and completion consulted. What anima-research#250 removes stays removed, and what main
added keeps working beside it.

Conflicts:
- src/framework.ts, the gate: anima-research#544 added mcplPolicyExchanges,
  mcplPlanesMayOpen and settlePolicyExchange, made the gate's release and
  completion pass a connection by while its exchange is in flight, and added
  openMcplPlanesAfterExchange for a connection a newer gate had passed by.
  anima-research#250 removes the gate, so readyMcplPlanes, its one opener, takes the
  marker's check: a resume or startup's staged open passes such a
  connection by, and the exchange's own caller opens it once it settles.
  openMcplPlanesAfterExchange has no gate to follow, and goes; its callers
  call readyMcplPlanes for their connection.
- src/framework.ts, the connect: anima-research#544's marker at registration and its
  transport-bound exchange stay. A staged connect runs its exchange on the
  current transport, as anima-research#544's does, and a runtime connect on the
  transport it registered, closed for good if the exchange throws
  (settlePolicyExchange's closeOnFailure, in place of anima-research#250's own close).
  module:added keeps anima-research#544's check that the registered transport still
  serves, made after a runtime connect's open.
- src/framework.ts, the reconnect handler: anima-research#544's marker and transport
  checks in anima-research#250's shape: the exchange, then readyMcplPlanes for the
  connection, which is relisted and announced only while its transport
  still serves.
- src/mcpl/server-connection.ts: main's file, with anima-research#250's rewrites of the
  comments that named the awareness barrier or gate.
- README.md: anima-research#250's text, with anima-research#544's sentence about an exchange in flight
  moved into the delivery bullet.
- test/fixtures/reregistering-mcpl-server.mjs: main's, its header naming
  the three test files that use it.
- test/mcpl-awareness-barrier.test.ts stays deleted. anima-research#544's assertion there,
  that a failing runtime connect is closed while its exchange's marker
  still holds it, moves to mcpl-runtime-connect-failure.test.ts, where anima-research#250
  keeps those tests.

Outside the conflicts:
- test/mcpl-reconnect-registration.test.ts, anima-research#544's: its harness stood on
  the gate. Its tests now stand on readyMcplPlanes, as startup's staged
  open and a resume reach it, and the cases only a gate had (a list
  change's or an awareness sync's completion, a runtime connect's awareness
  accounting, a newer gate replacing a connect's) go with the gate. Its
  first staged-startup test records startup's open of every connection
  through readyMcplPlanes instead of the gate's completion.
- changelog.d: 0.23.0 folded modern-mcp-client.added.md,
  stop-reaches-every-cleanup.fixed.md and mcpl-reconnect-registration.fixed.md,
  so anima-research#250 no longer edits the first two. Its one-shot fragment says what
  changes from 0.23.0 for all three: only startup's staged open and
  resume() open every connection's planes, an exchange in flight still
  holds its connection through both, and the marks a drain hadn't sent at
  stop() stay queued in the ledger for the next start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru pushed a commit to ajaniramon/agent-framework that referenced this pull request Oct 19, 2026
main's anima-research#562 (a render still unread stays when a later render or notice
arrives, and a retraction or plain occurrence takes the whole run) changed
the coalescer's occupant, its retraction, plain replacement and notice
paths, its render settlement and its snapshot, where this branch adds the
first-view mark, so seven places conflict in src/mcpl/push-coalescer.ts.
Each keeps main's runs and this branch's first view:

- The occupant is main's, a run of renders carried as `before`, and it
  carries this branch's `firstView`. A run's members keep their whole
  records, so a member keeps its mark.
- The render settlement keeps this branch's rule that a render is the
  agent's first view only when no other version of the subject is placed,
  so a render that joins a run is never marked: only a run's oldest render
  can carry the mark.
- occupantLeaves asks of the whole run. It hands a first view on only when
  the run's oldest render carries the mark and no render of the run may
  have been shown, and otherwise it ends the subject's first view. A
  retraction and a plain occurrence ask it before main's walk removes the
  run, since the removal takes the placements it reads.
- A notice beside an unread run leaves the run in place, as on main, and
  its batch is no first view, since the run holds it.
- The snapshot keeps both sides' fields.

Tests: this branch's tests of a notice displacing a queued render described
what main no longer does. They now check that the render stays and a later
render joins its run unmarked, and that a plain edit or a retraction that
takes the run hands its first view on, or ends it, by whether anything of
the run may have been shown. main's render-run stub host answers isUnshown
as it answers isUnread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 19, 2026
main brings anima-research#463's edit planner and edit_many, anima-research#350, anima-research#562, anima-research#563 and anima-research#566.
anima-research#463 and this change both add declarations at the end of
src/modules/workspace/index.ts, and both stand. Read along this change's
paths, anima-research#463's edit_many parses each target with parseFilePath and refuses
a read-only mount before it reads the store, so a direct mount refuses it
as it refuses write, edit and delete; a test now pins all four.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Coalescer: a retraction leaves an earlier render queued when two renders of one subject land in the same deferred queue

1 participant