Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3ec6915
fix(shards): a sharded body keeps each block in place and goes out as…
Oct 22, 2026
fcdf6b4
fix(shards): a tool result stays with its call wherever its message h…
Oct 22, 2026
d95fb9e
Merge main (c8119361) into the body-blocks-in-place change
Oct 24, 2026
4ab977c
docs(shards): three comments still said the middle's shard path sends…
Oct 24, 2026
757e501
test(pins): a marked message's parts are priced cut, as the shard pat…
Oct 24, 2026
8953ff8
Merge main (4fbe6c94) into the body-blocks-in-place change
Oct 24, 2026
7236560
test(thinking-binding): a thinking-led reply long enough to shard kee…
Oct 24, 2026
860dfdc
Merge main (1fe572ee) into the body-blocks-in-place change
Oct 25, 2026
ae6f358
fix(shards): no chunk or head window's end parts a call from the mess…
Oct 25, 2026
5713644
fix(shards): pair calls and results by id, in one pass, and keep a ca…
Oct 25, 2026
d4ecc4e
fix(shards): the recent window's start keeps a call it would part fro…
Oct 25, 2026
fc58539
fix(shards): keep a call with all of its results, carried across the …
Oct 25, 2026
2ecba6a
test(shards): a walk over a long run of unchunked messages at once ke…
Oct 25, 2026
b1ad54d
fix(shards): calls a message makes join those still waiting, the floo…
Oct 25, 2026
60395e2
Merge main (1a9e1b71) into body-blocks-in-place
Oct 26, 2026
044d007
fix(shards): hold a call for compression with the whole message after…
Oct 26, 2026
428dfba
chore(shards): cut the comments this pull request adds to the kinds t…
Oct 26, 2026
885fe04
Merge main (cedab243) into body-blocks-in-place
Oct 27, 2026
c42388e
fix(shards): keep a reset head out of a reply that makes a call, and …
Oct 27, 2026
def320c
docs(shards): state when a reply that makes a call still splits by th…
Oct 27, 2026
e6a0888
refactor(shards): take out the call-run walk's high-water marks
Oct 27, 2026
fb31126
Merge main (d82ee000) into body-blocks-in-place
Oct 27, 2026
2843fb3
docs(shards): name the reset split for stores whose chunks begin part…
Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions changelog.d/829-body-blocks-in-place.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
- **Hosts that join a sharded body's stored shards themselves:** where
`adaptiveResolution` is on, a message ingress splits into shards now keeps
each of its blocks in place. Each shard holds its slice of the message's
blocks, in order: its text blocks keep their own boundaries, cut where a
seam falls inside one, and every other block (an image, a document,
thinking, a tool call or result) is on the shard holding its place in the
text, so a reply ending in a tool call has the call on its last shard.
Every shard after the first declares `continuesBlock`, whether the seam
before it fell inside a text block. Before, a body's text was joined into
one block across its shards and every other block was on its first shard,
after its text.
- **Who needs to act:** whatever renders a body from its stored shards
rather than from a compile. Join them with `joinBodyContent`, now
exported, as the strategy's raw renders do. It gives a body stored this
way back as it was given, from shards that keep their `continuesBlock`,
and one stored before as the windows always sent it: its other blocks
first, then its text as one block. Where a caller joined shards that way
(`[...others, { type: 'text', text }]`), that is now
`joinBodyContent(shards)`. A caller that merged adjacent text blocks
across shards gets one block where two text blocks were given, and on
moving to `joinBodyContent` shows a body stored before in the windows'
order rather than in its shards' order. `joinBodyContent` and
`continuesBlock` require this release or later, so raise the dependency
floor with the change.
- **Two such callers:** agent-framework's `asCompiled`, which carries a
deferred write's drained copy as later compiles render it, joins as the
windows did, so for a body stored this way it puts the other blocks
ahead of all the text, where compiles send the body as given. Its change
is `joinBodyContent(landed)`, with each stored copy carrying
`continuesBlock`. connectome-host's Web UI (`coalesceAndFlatten`) merges
every adjacent pair of text blocks across a body's shards, so it shows
two given text blocks as one. Its change is to carry `continuesBlock` on
its message type, to take a body's run as the shards that continue it
(`continuesBody`, pairwise in store order) rather than every adjacent
shard with an equal `bodyGroupId`, and to join each run with
`joinBodyContent`. The same text ingested twice in a row shares one
`bodyGroupId`, so grouped by that id the two bodies go to one join.
- **Refused:** an override of `chunkIngressMessage` whose shards declare
`continuesBlock` on some shards after the first and not others is
refused before any shard is written. A first shard's declaration is
stored as given, and no join uses it.
- **Unchanged:** bodies stored before keep their shards as they are. The
seams fall where they did, so each shard holds the same text as before,
and a body keeps its `bodyGroupId`. `concatBodyGroups` still renders a
body as text alone, with a placeholder for each other block. A release
before this one renders a body stored this way as it renders its own
sharded bodies: in the windows its other blocks first and its text
joined, and in the middle its text alone.
96 changes: 96 additions & 0 deletions changelog.d/829-body-blocks-in-place.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
- Where `adaptiveResolution` is on, a message ingress splits into shards
goes out as it was given, wherever its shards render: every block in its
place, and its text blocks with their own boundaries, in one entry. After
a head-window reset, a body that leaves no call waiting and runs across
the head's end still goes out in pieces around the history from before the
reset, as before, and so can a reply that makes a call, in a store whose
chunks begin partway through it, until the next reset. A text block cut
across two shards comes back as one block of plain text, as cut text
always did. The head and recent windows sent the body's other blocks ahead
of its text and its text blocks as one, so a reply's tool call went out
before its text. The adaptive middle sent its text alone, so a reply
ending in a tool call lost the call and its result, an image went unsent,
and a tool result whose message was split went out as the placeholder for
a result that isn't shown. And a body whose shards fell in the middle and
a window went out as two or three entries, which membrane's native
formatter sends as one message with the body's text cut where the regions
meet, so each time the recent window's edge crossed one of its seams the
prompt was rewritten from that body on. The middle now sends each raw
shard as any raw message is sent, cut at `maxMessageTokens` as the windows
cut it and priced as the plan priced it, and the image strip judges each
image at the shard that holds it.
- A body stored before renders as it did wherever the head and recent
windows hold it. In the middle it now sends its other blocks as the
windows always did, ahead of its text, and across the middle and a window
it is one entry. A stored window holding such a body raw in the middle
with other blocks, or across the middle and a window, misses the prompt
cache from that body on, on its first request after the upgrade, and so
does one whose recent window starts at a tool result whose call is such a
body, or whose edge falls on a later shard of a split tool-result message
(next item).
- The recent window's start, the head and tail reservation's floor and the
compression hold's boundary keep a tool result with its call wherever the
message at the boundary holds the result, on any of its shards, starting
at the call's first shard that holds a `tool_use` or thinking, and, where
parallel calls' results come one to a message, at the call those results
answer. A message that answers only its own calls moves none of them back
past its own start, and a result whose call is neither in the message
before it nor among the parallel calls whose results come before it moves
none of them: the recent window stepped back one message at any result,
and the floor and the hold to the start of the message before one on its
first shard. The recent window started at a split result message's later
shard, leaving the result and its call out of the raw tail, and at a split
call's last shard, leaving its `tool_use` out when the call was stored
before blocks were kept in place. Tool-result pruning counts a sharded
reply's calls as one turn, wherever they sit in it, and a result in its
own call's message as answering nothing, on whichever of the message's
shards the two sit, as it did for a message stored whole.
- The split fallback's cuts, the head window's end and the recent window's
start now keep a tool call and all of its results on one side, wherever
their shards hold the `tool_use` and the `tool_result`s: the call stays
with each message after it that answers one of its calls, up to the first
that answers none, as when parallel calls' results come in messages of
their own, and a message that answers one and makes calls of its own adds
them to those still waiting. Ingress keeps each block in place, so a
result after a long text sits on its message's last shard, a call stored
before blocks were kept in place has its `tool_use` on its first shard and
its text on the shards after it, a call whose text follows its `tool_use`
has the same shape, and a message that makes a call and answers it itself
can hold the two on different shards. The head's end stepped back only
past a message holding a `tool_use`, the recent window's start stepped
back only from a message holding a result, and the split fallback kept
together only a message holding a `tool_use` and the result messages right
after it, so a piece or a window could end between a call and a result of
it, and a recent window starting between parallel calls' results could
refuse to compile (`UncoveredDropError`). Where each message answers the
call before it and makes the next, the recent window's start steps back
past the call its edge's result answers to the call before that one, and
doesn't follow the chain further, since each step adds raw messages to the
window: following a long chain to its start refused at budgets that
compiled before. The earlier call is parted as before. The head's end
stops outside a body that leaves a call waiting for a later result: where
it would fall inside one, or just after one whose call sits on its last
shard or is answered by the next message, it steps back to the body's
start, every shard of it, where it stepped back one message. So after a
head-window reset a reply that thinks and makes a call goes out whole
after the history from before the reset. As before: a result that comes
after a message answering none of the calls; a chunk closed where a pinned
message, the head or an earlier chunk interrupts the compressible
messages, and the Knowledge chunker's last, unclosed chunk; and the head's
end moving forward to where chunks resume (#122). Chunk records and
summaries already made are kept as they are; the Knowledge chunker, which
keeps no chunk records, closes its chunks not yet summarized anew on each
pass. In a store without shards, the head's end and the recent window's
start move where they fell between a call and its results. Such a store,
and one whose recent window's edge falls inside a call stored as shards,
can rewrite its prompt once, from that edge, at its first compile after
upgrading.
- A chunk's close after a call now waits until the whole message after it is
in the chunk, every shard of it, and a message that makes a call and
answers it itself keeps the two in one chunk across its shards. That is
the rule for closing after a message that holds a `tool_use`, taken from a
message to its shards. It waited one stored message, a single shard, so a
result on a later shard of that message could fall in the next chunk; a
compression request drops a `tool_result` whose call isn't in it, so such
a result reached no summary. In a store without shards chunks close as
before, parallel calls' results one to a message included.
7 changes: 7 additions & 0 deletions changelog.d/829-render-as-newest-entry.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
- 0.23.1's `renderAsNewest` entry said the adaptive middle sends a sharded
body's text alone, uncut, and that a body across the middle and a window
goes out in more than one entry. The compile now sends such a body in one
entry, as the render gives it. A sharded body still goes out in more than
one entry only across a reset head window's end: one that leaves no call
waiting for a later result, and a reply that makes a call, in a store
whose chunks begin partway through it, until the next reset.
13 changes: 9 additions & 4 deletions docs/adaptive-resolution-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,10 @@ Any message whose token count exceeds `chunkThreshold` (default 8192 tokens) is
- A stable `bodyGroupId` shared with its sibling shards.
- A `shardIndex` (0-based position within the group) for byte-faithful ordering at render time.
- The same `participant` as the original message.
- Its slice of the message's blocks, in order. The chunker cuts the message's text blocks joined into one string; each text block keeps its own boundaries, cut where a seam falls inside it, and every other block (an image, a document, thinking, a tool call or result) goes on the shard holding its place in the text: the shard whose text it comes before, or the last shard at the message's end. So a reply ending in a tool call keeps the call on its last shard, next to its result.
- On every shard after the first, `continuesBlock`: whether the seam before it fell inside a text block. A render joins the two pieces only there, so joined shards are the message as given (`joinBodyContent`), apart from a cut text block, which comes back as plain text: its other fields, such as cache_control, are the whole block's. A raw replay form a cut block carried, or one whose blocks land on more than one shard, is given up at ingress, so a shard never replays what another shard holds.

A body stored before blocks were kept in place has its text joined into one block, cut across its shards, and every other block on its first shard after its text, with no `continuesBlock`. Where those blocks sat wasn't kept, so wherever two or more of its shards render together, in any region, they render as the windows always joined them: its other blocks first, then its text as one block. A shard rendered alone goes out as stored.

The chunker (`src/adaptive/chunker.ts`) additionally computes a per-shard
`sourceHash` and `{ startByte, endByte }` range, but those live in the
Expand All @@ -218,7 +222,7 @@ the same shard boundaries.
**Chunker strategy** (V1, simple): structural-first (markdown headings, code-fence boundaries, blank-line paragraph breaks), token-bucket fallback (default `chunkSize` = 4096 tokens, no overlap), last-resort hard split at the largest UTF-8-safe position when no structural seam exists. Deterministic: re-ingesting identical content produces identical shards with identical `sourceHash`es. Byte-faithful: concatenating shards in order reproduces the original message body byte-for-byte.

**Rendering** (revised — see §3.6.1 for the implemented model):
- **Unfolded run** of consecutive same-`bodyGroupId` shards at L0 → one composite API message whose body is the byte-faithful concatenation of those shards. The role is the group's role. KV cache preserved across the run.
- **Unfolded run** of consecutive same-`bodyGroupId` shards at L0 → one composite API message of those shards' blocks, joined by `joinBodyContent`: for a whole body, the message as given. Each shard is sent as any raw message is, cut at `maxMessageTokens`, so a run renders the same in every region, and a run whose shards fall in two regions (the middle and a window) is still one message. After a head-window reset, a body that leaves no call waiting for a later result and runs across the head's end goes out in pieces around the history from before the reset, and so can a reply that makes a call, in a store whose chunks begin partway through it, until the next reset. The role is the group's role. KV cache preserved across the run.
- **Folded run** (consecutive shards under the same L_k ancestor) → emitted as a standard Q+A recall pair (one Context Manager question + one summaryParticipant answer carrying the summary text). The same format used for chat summaries elsewhere — consistent agent experience.

A bodyGroup with mixed resolutions therefore renders as multiple alternating entries: raw composite User message → recall pair → raw composite User message → recall pair → … The KV cache breaks at the fold boundaries (where the byte sequence changes anyway), but raw runs between folds stay cache-stable.
Expand All @@ -231,7 +235,7 @@ This was a deliberate departure from rev 2 of this doc, which proposed one compo
### 3.6.1 BodyGroup rendering algorithm

Within a single bodyGroup, walk shards in source order accumulating "runs":
- `raw` run = consecutive shards at L0; flushed as one User message with concatenated text.
- `raw` run = consecutive shards at L0; flushed as one message of their joined blocks.
- `summary` run = consecutive shards under the same L_k ancestor; flushed as a Q+A pair (deduped: same ancestor never emits twice).

A run breaks (and the previous run flushes) when:
Expand Down Expand Up @@ -396,8 +400,9 @@ The tail (recent window) is defined by token count from the latest chunk backwar
Algorithm: chunks transitioning out of the tail default to `currentResolution: 0`. They become eligible for raising on the next compile if pressure requires it. No special handling beyond the picker's normal eligibility check.

Edge cases:
- **A single chunk larger than K tokens**: the tail expands to include the whole chunk, even past K. The picker can't fold the most recent chunk ever — only previous ones. Combined with `bodyGroupId` chunking at ingestion, this is bounded by `chunkSize`, not message size.
- **Chunks at the tail boundary mid-bodyGroup**: the tail is defined over chunks, not over logical messages. A bodyGroup may straddle the boundary — its newer shards are tail-protected, older shards are picker-eligible. Render concatenates them back into one API message regardless.
- **A single chunk larger than K tokens**: the tail expands to include the whole chunk, even past K. The picker can't fold the most recent chunk ever — only previous ones. Combined with `bodyGroupId` chunking at ingestion, this is bounded by `chunkSize`, not message size, except where a tool call is held: a chunk holding a call runs to the end of the message after it, and through a message's own call to its result.
- **A tool call and its results at the tail boundary**: the window's start steps back to keep a call with all its results, wherever their shards hold them and when parallel calls' results come one to a message, so the tail can run past K by that call and the results before the edge. Where each message answers the call before it and makes the next, it steps back past the call its edge's result answers to the call before that one, and doesn't follow the chain further.
- **Chunks at the tail boundary mid-bodyGroup**: the tail is defined over chunks, not over logical messages. A bodyGroup may straddle the boundary — its newer shards are tail-protected, older shards are picker-eligible. Render concatenates them back into one API message, but for the reset case under Rendering above.

## 4. Operations

Expand Down
17 changes: 11 additions & 6 deletions src/adaptive/picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,12 +163,17 @@ export interface OverBudgetRawPin {
* outside the head, which stay raw wherever the window starts. That's the
* newest conversation message, a system change being none, with the
* changes after it (for one split into shards, its newest shard, since the
* earlier shards can fold), unless the newest message's body holds a tool_result, on its first
* shard: then it's that whole body and the whole call before it, which the
* result must follow raw. A smaller
* `recentWindowTokens` (the hot `tailTokens`) moves the rest out of the raw
* window into the middle, which folds once its chunks have closed and
* compression has summarized them.
* earlier shards can fold), unless the newest message's body holds a
* tool_result, on any of its shards: then it's that whole body, and with it
* the call the result answers where that call was made before the body, in
* the message before it or across parallel calls' results before it, from
* the call's first shard that holds a tool_use or a thinking block
* (thinking or redacted_thinking), which the result must follow raw; and
* where the message that made the call answers a call before it, as in a
* chain, from that earlier call, as the recent window's start counts it.
* A smaller `recentWindowTokens` (the hot `tailTokens`) moves the rest out
* of the raw window into the middle, which folds once its chunks have
* closed and compression has summarized them.
*
* Each list describes the refusal that was hit. Clearing it can bring the
* next one: a smaller recent window can leave the picker waiting for
Expand Down
Loading
Loading