Skip to content

feat(context): bound what a tool result contributes to a request - #2203

Open
mattzcarey wants to merge 32 commits into
mainfrom
feat/context-intake-shaping
Open

feat(context): bound what a tool result contributes to a request#2203
mattzcarey wants to merge 32 commits into
mainfrom
feat/context-intake-shaping

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Stacked on #2196. Implements the read-path half of #2201.

What

shapeMessage and shapeHistory bound what a stored tool result contributes to a model request:

  • a byte cap and a line cap, pi's 50 KB / 2,000 lines by default
  • truncation that always carries a continuation hint naming the offset to resume from
  • dropFields, a set of host-named fields to strip
shapeMessage(message, { maxBytes: 50_000, dropFields: ["details"] })

Why the read path

Capping is lossy — whatever is cut, the model cannot recover it. So storage keeps the full result and the cap applies to the copy being assembled into a request. A limit can then change next month without having destroyed the bytes in the meantime.

That is the same line drawn everywhere else in this design: sessions is lossless, context shapes. It is also the answer to the open question in design/context.md about whether intake shaping runs before or after persist — attachment extraction is lossless so it runs before, in #2196; capping is lossy so it runs after, here.

Why this is safe for prompt caching

A cache hit needs a byte-identical prefix, which is why sliding history truncation is deferred (#2200) rather than moved here.

These limits are a function of ONE message: how large that tool result is and which fields it carries. The same message shapes to the same bytes on turn 3 and on turn 300, whatever surrounds it, so the prefix is stable. There is a test asserting exactly that.

dropFields comes from a measurement

Across 251,895 real messages from local Claude Code and pi transcripts, three crossed the 1.5 MiB row budget. Pi persists a raw provider payload beside the content it renders, and that duplicate alone accounted for 2.6 MB and 1.65 MB of two of them. Dropping redundant payloads is worth more than the line caps, and costs the model nothing because it never needed both copies.

Which field is redundant is the host's call, so the module names none itself.

Scope

  • Only tool parts are touched. A long assistant answer passes through untouched.
  • Images pass through at full resolution, deliberately. Downscaling re-encodes a user's own bytes, and buys less than it appears to since providers scale images down before tokenizing — so it saves upload bytes and latency, not context. A host that wants fewer image bytes in the window evicts the image rather than degrading it.
  • Nothing calls this yet. Whether a host wants a cap, and at what size, is a policy decision for the host.

Tests

9 new tests covering the caps, the continuation hint, line-before-byte ordering, dropFields, nested tool output, the pass-through cases, byte-budget cutting that never splits a surrogate pair, streaming, and prefix stability.

https://claude.ai/code/session_01MXpoKqJrUFTqgmDrehDti4


Devin Review

…to-a-capability

# Conflicts:
#	examples/next/README.md
#	pnpm-lock.yaml
… context

Sessions stored large content by truncating it and budgeted hydration by
stored bytes, so a pointer row costing 8 MiB of memory was charged the ~100
bytes it occupies on disk. It also owned prompt assembly, which is not
conversation storage.

Storage. Every table is now WITHOUT ROWID with a composite key and no
secondary index, ordered by a per-session `seq`, so a text append bills one
row instead of two. The attachment reference table is (session_id,
message_id, hash) and nothing else. An unchanged update writes nothing at
all: no row, no FTS churn, no reference rewrite, no event.

Offload. Media leaves the row at a size threshold wherever it appears,
including `data:` URLs nested in tool output. Everything else, prose
included, is offloaded largest-first only when the row cannot hold it, and a
row that still does not fit raises SessionMessageTooLargeError. Nothing is
truncated, and offloaded content reconstructs byte for byte. The aged-row
maintenance pass applies that same policy, so a drained legacy row ends up
exactly as if it had been written today.

Memory. getRecentHistory charges each row its stored bytes plus, when
reconstructing inline, the attachment bytes it re-inflates. That is the
difference between a budget that bounds disk and one that bounds the
isolate. Think and AIChatAgent both default to 32 MiB.

Context. Blocks, frozen prompts, and the skill and search providers move to
`agents/context`. Think declares them through a new configureContext() hook
and reaches them through `this.context`; configureSession() keeps compaction
and search. The Session handle stores messages and knows nothing about
prompts.

Hosts. AIChatAgent no longer loads the transcript in its constructor: the
legacy lift and one bounded hydration run at start, the live array mirrors
the change feed, and get-messages streams. Think reads pointers on its
per-tool-result scan and no longer reads Sessions tables with raw SQL.

Deleted: the synchronous storage aperture, the lossy eviction mode,
sanitizeToolPairs, the token-counter plumbing, the per-field option thunks,
and a duplicate copy of the sanitize helpers.

Measured on a deployed worker with a real R2 bucket
(examples/next/sessions-slam, 33 scenarios): one billed row per append,
1.6 MB and 5 MB text parts and a 3 MB tool output offloaded rather than cut,
inline hydration stopping at 31.11 MiB against a 32 MiB budget, and 61.79
MiB of history streamed out of a 128 MiB isolate. Numbers are recorded in
design/sessions.md.

Claude-Session: https://claude.ai/code/session_01MXpoKqJrUFTqgmDrehDti4
agent-think lives outside packages/ and examples/, so it was missed when the
context system moved out of the Session handle. Its identity block is now
declared through configureContext() and refreshed through this.context.

Claude-Session: https://claude.ai/code/session_01MXpoKqJrUFTqgmDrehDti4
Renaming each lifted table to `*__lifted_v1` left every upgraded object
storing its conversation history twice. A Durable Object tops out at 10 GB
and gets uncomfortable well before that, so a 5 GB history plus its copy has
nowhere to go. The copy already needs that space transiently, which is
exactly why it must not be kept.

Each source is now verified against its destination row by row, comparing
the payload rather than just the key, and dropped only when every row
arrived intact. A table that fails verification is left in place with a
`session:migration:incomplete` event, so a partial lift keeps the only copy
of its rows instead of destroying it. `assistant_sessions` and
`assistant_fts` carry nothing the new schema needs, the registry being
derived and the index rebuilt, so they are dropped outright.

Think lifts `assistant_config` into its own table and now drops it too.
AIChatAgent drops `cf_ai_chat_agent_messages` once every readable row has a
copy, and its lift no longer reads the whole table into the isolate: order is
read as ids alone, then bodies are fetched in windows bounded by rows and
bytes, so a large transcript never lands in memory at once.

Verified against real deployed storage by seeding a Think agent and an
AIChatAgent on the pre-Sessions SDK and redeploying the identical worker
built against this branch over the same objects. Think carried 5 rows and
67,184 bytes across with its branch topology and ids intact, AIChatAgent 4
rows and 53,765 bytes including a 53 KB inline image, both byte-identical,
with no legacy or tombstone table left behind.

Claude-Session: https://claude.ai/code/session_01MXpoKqJrUFTqgmDrehDti4
…oviders

Storage classified payloads by content type: media left the row at a 32 KiB
threshold, everything else only to keep the row under its budget. Reading a
PDF through pi settles that this is backwards. A document arrives as plain
tool-output text with no media type, so the rule optimised the small case and
ignored the large one. Deduplication, the other argument for it, does not
depend on type either.

Extraction into the attachment tables does not make the database smaller:
chunk rows live in the same Durable Object, inside the same 10 GB. Only R2
reclaims space. And billing counts rows written, not bytes, so rewriting a
500 KB row costs the same single row as a tiny one while extracting it costs
four.

So there is now one rule and no content types in it. A payload is extracted
when a bucket is configured and it reaches `r2ThresholdBytes`, which is the
only extraction that reclaims anything, or when the row cannot otherwise hold
it, largest first, into chunks. `inlineThresholdBytes` is gone and the R2
threshold is the single number. The maintenance pass returns immediately
without a bucket, because inline is then the correct resting place.

Separately, the skill-provider path is deleted: `R2SkillProvider`,
`isSkillProvider`, the load and unload skill state on `ContextBlocks`, the
`load_context` and `unload_context` tools, and the helpers that replayed that
state from the transcript. Nothing ever registered a provider with a `load()`
method. Think shipped its own Agent Skills through `agents/skills` before this
path had a user and registers a plain readonly catalog block, so the tools
never appeared and the history scan always returned immediately. A dead prompt
line telling the model to use context-loading tools went with it.
`agents/context` drops from 1,387 lines to about 880.

Think's media eviction is untouched. It decides what the model sees, which is
a different question from where bytes live.

Claude-Session: https://claude.ai/code/session_01MXpoKqJrUFTqgmDrehDti4
Sessions is a message store, not a file store. A message can reference a
file without being one, and hosts that handle files already have somewhere
to put them: Think's Workspace spills to R2 at 1,500,000 bytes, the same
threshold the session tier used, so the two were doing one job twice.

With R2 gone there is no reason to extract a payload eagerly at all, because
chunk rows live in the same Durable Object and never reclaim a byte. The rule
is now one sentence with no configuration: a payload stays in its message row
until the row cannot hold it, and then the largest payloads are chunked out
until it fits.

That takes the aged-row maintenance pass with it. Its only remaining job was
draining rows into R2, so the pass, its scheduler, its backlog chaining, the
`offload_candidate_bytes` column and the four core methods that stamped and
read it are all gone. Think never used it: it disables the pass whenever its
own media eviction is on and drives that from a truncated hydration read.

Also removed: the R2 bucket port, key construction and cleanup, the
declared-versus-unknown-length streaming split and `FixedLengthStream`, the
`backend` and `r2_key` blob columns, and the in-memory bucket fakes three
packages carried to observe a tier that no longer exists.
`SessionsAttachmentOptions` goes from seven fields to three.

Think's media eviction is untouched in behavior. It decides what the model
sees, which is a different question from where bytes live, and it is what
moves bytes to the Workspace.

The sessions-slam example is deleted along with the measured table it fed, so
the docs no longer quote numbers nothing can reproduce.

Found in passing: ai-chat's test worker was inserting into a column renamed
some time ago, which surfaced as seven swallowed unhandled rejections in a
passing run.

Adds design/context.md, recording that prompt assembly and history shaping
belong in agents/context while retention stays with the host that owns the
file store, and that shaping tool output at the boundary is the missing third
piece (#2201).

Claude-Session: https://claude.ai/code/session_01MXpoKqJrUFTqgmDrehDti4
Media declared with a non-text media type and carried inline is stored
separately, addressed by SHA-256, and inlined again on read. The message
keeps a pointer and its mediaType, so a round trip is exact and a row
stays small however large its payloads are. Read with
{ attachments: "pointer" } to see references instead.

The rule is typed rather than sized. An image is extracted at any size
and text is never extracted at any size, so a message's stored shape
never depends on how large an image happened to be. Row chunking stays
as the independent size backstop for prose: the two never interact,
because media leaves before the row is measured.

This is not the layer that was removed. That one extracted only when a
row was over budget, which made it a rescue mechanism competing with
chunking and one that could fail with nothing to extract. 775 lines
replace 1,477.

Payload lifetime is derived from reference rows, taken from the stored
message rather than from what a given write extracted, so a pointer-mode
read written back keeps its payload alive. SessionRowStat.bytes charges
each message for what it points at, at inlined size, so a byte budget
still bounds real hydrated memory.

Cost, measured: a 200 KB image bills four rows and a 2 MiB image five,
against one when inlined. Text messages are unchanged at one row.

Also adds agents/context intake shaping. shapeMessage/shapeHistory cap
oversized tool results with a continuation hint and drop host-named
duplicate fields, on the read path so storage stays lossless. The limits
are a function of one message, so a shaped prefix stays byte-identical
across turns and prompt caching holds — which is why this landed here
and sliding history truncation stayed with the hosts.

Claude-Session: https://claude.ai/code/session_01MXpoKqJrUFTqgmDrehDti4
Pi resizes images on the way into context. We deliberately do not, and
this records why rather than leaving it as an open question: downscaling
re-encodes a user's own bytes, which is a lossy transform of content
nobody asked us to change, and it is the one kind of shaping a host
cannot undo afterwards. Text caps and duplicate-field dropping stay.

Claude-Session: https://claude.ai/code/session_01MXpoKqJrUFTqgmDrehDti4
Intake shaping is a separate concern from keeping attachments out of the
message, and reviewing them together obscures both. It lands on a branch
stacked on this one; nothing here depends on it.

Claude-Session: https://claude.ai/code/session_01MXpoKqJrUFTqgmDrehDti4
shapeMessage and shapeHistory apply a byte cap, a line cap, and a set of
host-named fields to drop, to the tool parts of a message. Truncation
always carries a continuation hint naming the offset to resume from, so
an aggressive cap is a detour rather than a dead end — which is what
lets pi cap roughly seventy times harder than Think does today.

It runs on the READ path. Capping is lossy, so storage keeps the full
result and the limit stays a policy that can change without having
destroyed anything. That is the same line drawn everywhere else here:
sessions is lossless, context shapes.

Only tool parts are touched. A long assistant answer passes through, and
so does an image — downscaling re-encodes a user's own bytes and buys
less than it looks like, since providers scale images before tokenizing.

dropFields exists because of a measurement: pi persists a raw provider
payload beside the content it renders, and that duplicate alone was
2.6 MB and 1.65 MB in two of the three real messages that crossed the
row budget. The module names no fields itself; only a host knows which
of its own are duplicates.

Nothing calls this yet — whether a host wants a cap, and at what size,
is the host's decision to make.

Claude-Session: https://claude.ai/code/session_01MXpoKqJrUFTqgmDrehDti4
@changeset-bot

changeset-bot Bot commented Sep 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4931795

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
agents Patch
@cloudflare/agent-think Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 2 potential issues.

3 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

depth: number
): unknown {
if (depth > 8) return value;
if (typeof value === "string") return capText(value, limits);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Multi-string results bypass intake limits

When one result contains multiple strings, shapeValue gives each string the full byte and line budgets. The request can remain arbitrarily large.

Prompt for agents
The limits documented by IntakeLimits and shapeMessage are per tool result, but shapeValue invokes capText independently for every nested string. Track shared remaining byte and line budgets while traversing each output/result value, preserving deterministic traversal and copy-on-write behavior. The continuation metadata must describe the actual cut point, and tests must cover objects and arrays containing multiple individually under-limit strings whose combined content exceeds each limit.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +117 to +120
let end = head.length;
while (end > 0 && byteLength(head.slice(0, end)) > maxBytes) end--;
const code = head.charCodeAt(end - 1);
if (code >= 0xd800 && code <= 0xdbff) end--;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Large results exhaust Worker CPU

For oversized text, capText re-encodes the shrinking prefix after removing every UTF-16 unit. Multi-megabyte results can exhaust Worker CPU.

Prompt for agents
capText currently finds the byte boundary by decrementing a UTF-16 index and encoding the full prefix on every iteration, making truncation quadratic. Replace this with a linear or logarithmic boundary search, such as binary search over UTF-16 indices with surrogate-boundary adjustment or a single UTF-8 encoding followed by safe decoding. Preserve the maxBytes contract, valid surrogate pairs, exact byte-based nextOffset, and deterministic output. Add a multi-megabyte regression test or focused performance assertion.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

…to-a-capability

# Conflicts:
#	design/rfc-think-multi-session.md
…ions losing rows

Three review findings, all real.

The hydration budget was not a bound. `getRecentHistory` admitted rows
whenever fewer than `minRecentMessages` had been taken, whatever they
weighed, so a window of media-heavy messages hydrated far past the limit
that was supposed to cap it. A floor that ignores size is not a floor
under a budget, it is a hole in one. The parameter is gone from core,
handle, Think and AIChat; the budget is a hard ceiling that always
returns at least the newest message.

Think's window can therefore be shorter than MODEL_RECENT_WINDOW when
messages are unusually large. That is the intended trade: with a 32 MB
budget it only ever binds on media, and 32 MB of text is far past what
any model could read anyway.

AIChat's legacy lift could delete history. It dropped the source table
when `imported + skipped === order.length` — but a skipped row is one
that could NOT be parsed or imported, so accounting for it and migrating
it are not the same thing, and any malformed row was destroyed. It now
drops only when every row actually landed, and says what it kept.

Sessions stamped its schema version even when `migrateLegacy` reported
an incomplete copy, so the lift never retried and the rows it left
behind stayed unreachable. `migrateLegacy` now reports completeness and
the version is stamped only on success. Both lifts are idempotent, so
retrying costs reads and nothing else.

Also resolves the conflict with main in rfc-think-multi-session.md,
taking main's text, which records the same supersession plus the
replacement RFC.

Claude-Session: https://claude.ai/code/session_01MXpoKqJrUFTqgmDrehDti4
@agent-think

agent-think Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🔴 agents import sizes

Measured 287 runtime imports as minified bundles. The primary size is gzip; raw minified size is included for diagnosis. An existing import growing by more than 10% is marked red. This report is informational.

Red Yellow Green Unchanged New Removed
2 3 0 277 5 0

Compared 5d33387e with 4931795b. Open workflow run.

Changed imports (10)
Status Import Base gzip Head gzip Delta
🔴 agents/context#isWritableProvider 112 B 129 B +17 B (+15.18%)
🔴 agents/context#isSearchProvider 116 B 133 B +17 B (+14.66%)
🟡 agents/context#AgentContextProvider 412 B 427 B +15 B (+3.64%)
🟡 agents/context#AgentSearchProvider 640 B 655 B +15 B (+2.34%)
🟡 agents/context#ContextBlocks 87.7 KiB 87.7 KiB +12 B (+0.01%)
agents/context#capText 430 B
agents/context#DEFAULT_MAX_TOOL_OUTPUT_BYTES 75 B
agents/context#DEFAULT_MAX_TOOL_OUTPUT_LINES 75 B
agents/context#shapeHistory 791 B
agents/context#shapeMessage 761 B
All 287 current runtime imports
Status Import Gzip Raw minified
agents#__DO_NOT_USE_WILL_BREAK__agentContext 258.6 KiB 1130.0 KiB
agents#__DO_NOT_USE_WILL_BREAK__withInvocationScope 258.6 KiB 1130.0 KiB
agents#Agent 258.6 KiB 1130.0 KiB
agents#AGENT_TOOL_MILESTONE_PART 258.6 KiB 1130.0 KiB
agents#AGENT_TOOL_PROGRESS_PART 258.6 KiB 1130.0 KiB
agents#buildAgentPath 259.1 KiB 1132.3 KiB
agents#buildAgentUrl 259.3 KiB 1132.7 KiB
agents#callable 258.6 KiB 1130.1 KiB
agents#camelCaseToKebabCase 258.6 KiB 1130.0 KiB
agents#createHeaderBasedEmailResolver 258.8 KiB 1130.4 KiB
agents#DEFAULT_AGENT_STATIC_OPTIONS 258.6 KiB 1130.0 KiB
agents#DurableObjectOAuthClientProvider 258.6 KiB 1130.0 KiB
agents#getAgentByName 258.6 KiB 1130.0 KiB
agents#getCurrentAgent 258.6 KiB 1130.0 KiB
agents#getSubAgentByName 258.9 KiB 1130.7 KiB
agents#isDurableObjectCodeUpdateReset 258.6 KiB 1130.0 KiB
agents#isDurableObjectMemoryLimitReset 258.6 KiB 1130.0 KiB
agents#isDurableObjectStorageReset 258.6 KiB 1130.1 KiB
agents#isPlatformTransientError 258.6 KiB 1130.0 KiB
agents#MCP_SERVER_ID_MAX_LENGTH 258.6 KiB 1130.0 KiB
agents#MessageType 258.8 KiB 1130.3 KiB
agents#normalizeServerId 258.6 KiB 1130.0 KiB
agents#parseSubAgentPath 258.6 KiB 1130.0 KiB
agents#routeAgentEmail 258.9 KiB 1130.7 KiB
agents#routeAgentRequest 259.2 KiB 1131.9 KiB
agents#routeSubAgentRequest 258.8 KiB 1130.6 KiB
agents#SqlError 258.6 KiB 1130.0 KiB
agents#StreamingResponse 258.6 KiB 1130.0 KiB
agents#SUB_PREFIX 258.6 KiB 1130.0 KiB
agents#unstable_callable 258.7 KiB 1130.2 KiB
agents/agent-tools#agentTool 112.5 KiB 538.2 KiB
agents/browser#BrowserConnector 50.5 KiB 176.6 KiB
agents/browser#browserContent 36.3 KiB 127.4 KiB
agents/browser#browserExtract 36.3 KiB 127.4 KiB
agents/browser#browserLinks 36.3 KiB 127.4 KiB
agents/browser#browserMarkdown 36.3 KiB 127.4 KiB
agents/browser#browserPdf 36.3 KiB 127.3 KiB
agents/browser#BrowserRenderingError 36.0 KiB 126.7 KiB
agents/browser#browserScrape 36.3 KiB 127.4 KiB
agents/browser#browserScreenshot 36.3 KiB 127.3 KiB
agents/browser#browserSnapshot 36.3 KiB 127.4 KiB
agents/browser#CdpSession 37.2 KiB 129.8 KiB
agents/browser#CodemodeRuntime 39.6 KiB 139.0 KiB
agents/browser#connectBrowser 37.8 KiB 131.4 KiB
agents/browser#connectBrowserSession 37.5 KiB 130.4 KiB
agents/browser#connectUrl 37.6 KiB 130.5 KiB
agents/browser#createBrowserSession 36.3 KiB 127.5 KiB
agents/browser#DEFAULT_EXEC_SWEEP_IDLE_MS 36.0 KiB 126.6 KiB
agents/browser#DEFAULT_SWEEP_IDLE_MS 36.0 KiB 126.6 KiB
agents/browser#deleteBrowserSession 36.1 KiB 126.9 KiB
agents/browser#DurableBrowserSessionStore 36.4 KiB 127.6 KiB
agents/browser#getBrowserRecording 36.2 KiB 127.1 KiB
agents/browser#listBrowserTargets 36.1 KiB 126.9 KiB
agents/browser#loadCdpSpec 36.6 KiB 128.3 KiB
agents/browser#runQuickAction 36.0 KiB 126.6 KiB
agents/browser/ai#createBrowserRuntime 146.0 KiB 630.3 KiB
agents/browser/ai#createBrowserTools 146.0 KiB 630.3 KiB
agents/browser/ai#createQuickActionTools 122.5 KiB 554.3 KiB
agents/browser/tanstack-ai#createBrowserTools 161.7 KiB 699.2 KiB
agents/chat#AbortRegistry 2.5 KiB 8.9 KiB
agents/chat#AGENT_TOOL_STREAM_PROGRESS_BUMP_THROTTLE_MS 2.3 KiB 8.2 KiB
agents/chat#AgentToolProgressEmitter 2.7 KiB 9.5 KiB
agents/chat#AgentToolStreamProgressThrottle 2.4 KiB 8.3 KiB
agents/chat#aiSdkRecoveryCodec 2.3 KiB 8.2 KiB
agents/chat#applyAgentToolEvent 3.2 KiB 11.0 KiB
agents/chat#applyChunkToParts 2.3 KiB 8.2 KiB
agents/chat#applyToolUpdate 2.4 KiB 8.4 KiB
agents/chat#AutoContinuationController 2.3 KiB 8.2 KiB
agents/chat#awaitWithDeadline 2.4 KiB 8.4 KiB
agents/chat#broadcastTransition 3.1 KiB 11.4 KiB
agents/chat#buildChatRecoveringFrame 2.4 KiB 8.4 KiB
agents/chat#buildInClauseStrings 2.4 KiB 8.4 KiB
agents/chat#bumpChatRecoveryProgress 2.4 KiB 8.3 KiB
agents/chat#byteLength 2.5 KiB 8.5 KiB
agents/chat#CHAT_LAST_TERMINAL_KEY 2.3 KiB 8.2 KiB
agents/chat#CHAT_MESSAGE_TYPES 2.3 KiB 8.2 KiB
agents/chat#CHAT_RECOVERING_FLAG_TTL_MS 2.3 KiB 8.2 KiB
agents/chat#CHAT_RECOVERING_KEY 2.3 KiB 8.2 KiB
agents/chat#CHAT_RECOVERY_ALARM_DEBOUNCE_MS 2.3 KiB 8.2 KiB
agents/chat#CHAT_RECOVERY_INCIDENT_KEY_PREFIX 2.3 KiB 8.2 KiB
agents/chat#CHAT_RECOVERY_INCIDENT_TTL_MS 2.3 KiB 8.2 KiB
agents/chat#CHAT_RECOVERY_PROGRESS_KEY 2.3 KiB 8.2 KiB
agents/chat#CHAT_RECOVERY_STABLE_RETRY_DELAY_SECONDS 2.3 KiB 8.2 KiB
agents/chat#CHAT_RECOVERY_TASK_NAME 2.3 KiB 8.2 KiB
agents/chat#CHAT_STREAM_PROGRESS_CREDIT_THROTTLE_MS 2.3 KiB 8.2 KiB
agents/chat#ChatRecoveryEngine 4.4 KiB 15.3 KiB
agents/chat#chatRecoveryTaskRunOptions 2.4 KiB 8.6 KiB
agents/chat#ChatStreamStalledError 2.4 KiB 8.3 KiB
agents/chat#classifyAgentToolChildRecovery 2.4 KiB 8.5 KiB
agents/chat#cleanupStreamBuffers 2.3 KiB 8.2 KiB
agents/chat#clearChatTerminal 2.3 KiB 8.3 KiB
agents/chat#clientResolvableToolNames 2.3 KiB 8.3 KiB
agents/chat#ContinuationState 2.7 KiB 9.8 KiB
agents/chat#createAgentToolEventState 2.3 KiB 8.3 KiB
agents/chat#createChatFiberSnapshot 2.5 KiB 8.6 KiB
agents/chat#createChatRecoveryTaskDefinition 2.6 KiB 9.0 KiB
agents/chat#createChatStreams 5.5 KiB 19.3 KiB
agents/chat#createChatTurnTaskDefinition 2.7 KiB 8.9 KiB
agents/chat#createToolsFromClientSchemas 114.3 KiB 545.4 KiB
agents/chat#crossMessageToolResultUpdate 2.4 KiB 8.6 KiB
agents/chat#DEFAULT_CHAT_RECOVERY_MAX_ATTEMPTS 2.3 KiB 8.2 KiB
agents/chat#DEFAULT_CHAT_RECOVERY_MAX_OOM_RETRIES 2.3 KiB 8.2 KiB
agents/chat#DEFAULT_CHAT_RECOVERY_MAX_WORK 2.3 KiB 8.2 KiB
agents/chat#DEFAULT_CHAT_RECOVERY_NO_PROGRESS_TIMEOUT_MS 2.3 KiB 8.2 KiB
agents/chat#DEFAULT_CHAT_RECOVERY_STABLE_TIMEOUT_MS 2.3 KiB 8.2 KiB
agents/chat#DEFAULT_CHAT_RECOVERY_TERMINAL_MESSAGE 2.4 KiB 8.3 KiB
agents/chat#dispatchChatRecoveryToHandoff 3.0 KiB 9.9 KiB
agents/chat#drainInteractionApplies 2.3 KiB 8.3 KiB
agents/chat#enforceRowSizeLimit 3.5 KiB 11.2 KiB
agents/chat#hasIncompleteToolBatch 2.4 KiB 8.6 KiB
agents/chat#interceptAgentToolBroadcast 2.5 KiB 8.7 KiB
agents/chat#isPlatformFailure 2.6 KiB 8.9 KiB
agents/chat#isReplayChunk 2.4 KiB 8.7 KiB
agents/chat#iterateWithStallWatchdog 2.6 KiB 8.8 KiB
agents/chat#KV_DELETE_MAX_KEYS 2.3 KiB 8.2 KiB
agents/chat#listActiveChatRecoveryIncidents 2.4 KiB 8.4 KiB
agents/chat#MAX_BOUND_PARAMS 2.3 KiB 8.2 KiB
agents/chat#MessageType 2.4 KiB 9.0 KiB
agents/chat#normalizeToolInput 2.3 KiB 8.2 KiB
agents/chat#parseProtocolMessage 2.5 KiB 9.0 KiB
agents/chat#partAwaitsClientInteraction 2.4 KiB 8.6 KiB
agents/chat#pausedExecutionUpdate 2.4 KiB 8.4 KiB
agents/chat#pendingChatTerminal 2.3 KiB 8.3 KiB
agents/chat#persistReconstructedOrphan 3.1 KiB 11.0 KiB
agents/chat#PreStreamTurns 2.6 KiB 9.3 KiB
agents/chat#readChatRecoveryProgress 2.3 KiB 8.3 KiB
agents/chat#reconcileMessages 2.8 KiB 9.6 KiB
agents/chat#reconcileOrphanPartial 2.4 KiB 8.5 KiB
agents/chat#recordChatTerminal 2.4 KiB 8.3 KiB
agents/chat#repairInterruptedToolParts 2.6 KiB 9.1 KiB
agents/chat#resolveChatRecoveryConfig 2.6 KiB 8.9 KiB
agents/chat#resolveToolMergeId 2.4 KiB 8.5 KiB
agents/chat#ResumableStream 4.6 KiB 15.3 KiB
agents/chat#ResumeHandshake 3.0 KiB 10.4 KiB
agents/chat#ROW_MAX_BYTES 2.3 KiB 8.2 KiB
agents/chat#runChatRecoveryExhaustion 2.6 KiB 8.9 KiB
agents/chat#sanitizeMessage 2.5 KiB 8.8 KiB
agents/chat#sendIfOpen 2.4 KiB 8.4 KiB
agents/chat#setChatRecovering 2.5 KiB 8.5 KiB
agents/chat#shouldCreditStreamProgress 2.4 KiB 8.3 KiB
agents/chat#STREAM_CLEANUP_DELAY_SECONDS 2.3 KiB 8.2 KiB
agents/chat#STREAM_RESUME_NONE_REASONS 2.3 KiB 8.3 KiB
agents/chat#StreamAccumulator 2.9 KiB 10.7 KiB
agents/chat#StreamProgressCreditThrottle 2.4 KiB 8.3 KiB
agents/chat#SubmitConcurrencyController 2.9 KiB 10.3 KiB
agents/chat#sweepStaleChatRecoveryIncidents 2.4 KiB 8.5 KiB
agents/chat#TextSegmentJoiner 2.7 KiB 9.2 KiB
agents/chat#TIMED_OUT 2.3 KiB 8.2 KiB
agents/chat#toolApprovalUpdate 2.4 KiB 8.5 KiB
agents/chat#toolPartHasSettledResult 2.3 KiB 8.4 KiB
agents/chat#toolResultUpdate 2.4 KiB 8.5 KiB
agents/chat#truncateOlderMessages 3.2 KiB 10.4 KiB
agents/chat#TurnQueue 2.6 KiB 9.2 KiB
agents/chat#unwrapChatFiberSnapshot 2.4 KiB 8.5 KiB
agents/chat#wrapChatFiberSnapshot 2.3 KiB 8.2 KiB
agents/chat-sdk#ChatSdkStateAdapter 261.0 KiB 1141.6 KiB
agents/chat-sdk#ChatSdkStateAgent 260.4 KiB 1139.1 KiB
agents/chat-sdk#createChatSdkState 261.0 KiB 1141.6 KiB
agents/chat-sdk#defaultKeyShard 258.8 KiB 1130.2 KiB
agents/chat-sdk#defaultThreadShard 258.7 KiB 1130.1 KiB
agents/chat/react#detectToolsRequiringConfirmation 3.3 KiB 8.3 KiB
agents/chat/react#extractClientToolSchemas 3.2 KiB 8.3 KiB
agents/chat/react#getAgentMessages 3.4 KiB 8.6 KiB
agents/chat/react#getToolApproval 3.1 KiB 8.0 KiB
agents/chat/react#getToolCallId 3.1 KiB 8.0 KiB
agents/chat/react#getToolInput 3.1 KiB 8.0 KiB
agents/chat/react#getToolOutput 3.1 KiB 8.0 KiB
agents/chat/react#getToolPartState 3.2 KiB 8.2 KiB
agents/chat/react#useAgentChat 132.9 KiB 609.7 KiB
agents/chat/react#WebSocketChatTransport 5.7 KiB 17.1 KiB
agents/chat/transport#WebSocketChatTransport 2.8 KiB 9.2 KiB
agents/client#AgentClient 5.7 KiB 16.6 KiB
agents/client#AgentConnectionError 582 B 993 B
agents/client#agentFetch 4.2 KiB 12.3 KiB
agents/client#createStubProxy 638 B 1.0 KiB
agents/client#DEFAULT_CALL_TIMEOUT_MS 473 B 770 B
agents/client#isTerminalCloseEvent 509 B 822 B
🟡 agents/context#AgentContextProvider 427 B 814 B
🟡 agents/context#AgentSearchProvider 655 B 1.4 KiB
agents/context#capText 430 B 749 B
🟡 agents/context#ContextBlocks 87.7 KiB 430.7 KiB
agents/context#DEFAULT_MAX_TOOL_OUTPUT_BYTES 75 B 55 B
agents/context#DEFAULT_MAX_TOOL_OUTPUT_LINES 75 B 55 B
🔴 agents/context#isSearchProvider 133 B 138 B
🔴 agents/context#isWritableProvider 129 B 132 B
agents/context#shapeHistory 791 B 1.5 KiB
agents/context#shapeMessage 761 B 1.4 KiB
agents/email#createAddressBasedEmailResolver 193 B 227 B
agents/email#createCatchAllEmailResolver 110 B 97 B
agents/email#createHeaderBasedEmailResolver 334 B 492 B
agents/email#createSecureReplyEmailResolver 718 B 1.3 KiB
agents/email#DEFAULT_MAX_AGE_SECONDS 56 B 39 B
agents/email#isAutoReplyEmail 201 B 249 B
agents/email#signAgentHeaders 424 B 812 B
agents/experimental/webmcp#registerWebMcp 85.2 KiB 295.8 KiB
agents/lifecycle#getCurrentAgent 376 B 798 B
agents/lifecycle#Lifecycle 8.3 KiB 25.8 KiB
agents/lifecycle#LifecycleCapability 484 B 975 B
agents/mcp#createLegacyMcpHandler 375.9 KiB 1572.2 KiB
agents/mcp#createMcpHandler 388.2 KiB 1617.4 KiB
agents/mcp#DurableObjectEventStore 342.5 KiB 1430.7 KiB
agents/mcp#ElicitRequestSchema 342.5 KiB 1430.7 KiB
agents/mcp#experimental_createMcpHandler 376.1 KiB 1572.5 KiB
agents/mcp#getMcpAuthContext 342.5 KiB 1430.8 KiB
agents/mcp#MCP_SERVER_ID_MAX_LENGTH 342.5 KiB 1430.7 KiB
agents/mcp#McpAgent 342.5 KiB 1430.7 KiB
agents/mcp#normalizeServerId 342.5 KiB 1430.7 KiB
agents/mcp#RPC_DO_PREFIX 342.5 KiB 1430.7 KiB
agents/mcp#RPCClientTransport 342.5 KiB 1430.7 KiB
agents/mcp#RPCServerTransport 342.5 KiB 1430.7 KiB
agents/mcp#SSEEdgeClientTransport 342.6 KiB 1431.0 KiB
agents/mcp#StreamableHTTPEdgeClientTransport 342.6 KiB 1431.0 KiB
agents/mcp#WorkerTransport 345.8 KiB 1447.6 KiB
agents/mcp/client#getNamespacedData 62.9 KiB 240.0 KiB
agents/mcp/client#MCP_SERVER_ID_MAX_LENGTH 62.9 KiB 239.9 KiB
agents/mcp/client#MCPClientManager 158.6 KiB 702.6 KiB
agents/mcp/client#normalizeServerId 63.0 KiB 240.2 KiB
agents/mcp/do-oauth-client-provider#DurableObjectOAuthClientProvider 2.1 KiB 6.6 KiB
agents/mcp/server#createMcpHandler 80.5 KiB 307.2 KiB
agents/mcp/server#getMcpAuthContext 64.0 KiB 245.5 KiB
agents/observability#channels 259 B 549 B
agents/observability#genericObservability 470 B 1.2 KiB
agents/observability#subscribe 324 B 668 B
agents/observability/ai#wrapAISDK 8.8 KiB 30.5 KiB
agents/react#_testUtils 3.8 KiB 9.5 KiB
agents/react#useAgent 10.8 KiB 31.1 KiB
agents/react#useAgentToolEvents 5.6 KiB 16.8 KiB
agents/routing#getAgentByName 795 B 1.7 KiB
agents/routing#routeAgentRequest 1.6 KiB 3.6 KiB
agents/routing#RoutedAgents 2.4 KiB 6.2 KiB
agents/schedule#getSchedulePrompt 85.8 KiB 424.7 KiB
agents/schedule#scheduleSchema 85.3 KiB 423.6 KiB
agents/schedule#unstable_getSchedulePrompt 85.9 KiB 424.9 KiB
agents/schedule#unstable_scheduleSchema 85.3 KiB 423.6 KiB
agents/schedules#Scheduler 6.8 KiB 22.0 KiB
agents/schedules/parser#getSchedulePrompt 85.8 KiB 424.7 KiB
agents/schedules/parser#scheduleSchema 85.3 KiB 423.6 KiB
agents/sessions#ATTACHMENT_CHUNK_BYTES 139 B 148 B
agents/sessions#ATTACHMENT_URL_PREFIX 152 B 167 B
agents/sessions#attachmentUrl 177 B 198 B
agents/sessions#COMPACTION_PREFIX 147 B 160 B
agents/sessions#createCompactFunction 1.7 KiB 4.0 KiB
agents/sessions#DEFAULT_SESSION_ID 139 B 149 B
agents/sessions#estimateAttachmentTokens 192 B 220 B
agents/sessions#estimateMessageTokens 413 B 684 B
agents/sessions#estimateStringTokens 218 B 262 B
agents/sessions#isCompactionMessage 177 B 200 B
agents/sessions#MAX_INLINE_ROW_BYTES 134 B 140 B
agents/sessions#parseAttachmentUrl 241 B 281 B
agents/sessions#Session 2.1 KiB 6.6 KiB
agents/sessions#Sessions 9.9 KiB 35.7 KiB
agents/sessions#SessionSearchDisabledError 258 B 349 B
agents/sessions#SessionSerializationError 216 B 260 B
agents/skills#fromManifest 309.8 KiB 1084.0 KiB
agents/skills#parseSkillFrontmatter 328.4 KiB 1146.2 KiB
agents/skills#parseSkillMarkdown 328.6 KiB 1146.5 KiB
agents/skills#r2 330.2 KiB 1150.4 KiB
agents/skills#runner 369.0 KiB 1297.8 KiB
agents/skills#SkillRegistry 416.4 KiB 1581.7 KiB
agents/skills/compile#compileSkillScript 15.4 KiB 43.4 KiB
agents/skills/compile#isCompilableSkillScript 15.4 KiB 43.3 KiB
agents/streams#DEFAULT_MAX_CHUNK_BYTES 83 B 81 B
agents/streams#sseResponse 843 B 1.6 KiB
agents/streams#StreamClosedError 161 B 197 B
agents/streams#StreamNotFoundError 201 B 261 B
agents/streams#Streams 3.4 KiB 11.1 KiB
agents/streams#StreamSerializationError 158 B 186 B
agents/tasks#DuplicateTaskStepError 328 B 463 B
agents/tasks#MAX_SERIALIZED_BYTES 190 B 232 B
agents/tasks#MissingTaskDefinitionError 358 B 536 B
agents/tasks#NonRetryableError 238 B 308 B
agents/tasks#TaskReplayDivergedError 341 B 483 B
agents/tasks#Tasks 8.9 KiB 31.8 KiB
agents/tasks#TaskSerializationError 258 B 339 B
agents/types#MessageType 211 B 365 B
agents/vite#default 353.8 KiB 1356.1 KiB
agents/websockets#CALLABLES_RPC_QUERY 12.4 KiB 43.3 KiB
agents/websockets#CALLABLES_RPC_VALUE 12.4 KiB 43.3 KiB
agents/websockets#callablesFromDecorated 12.7 KiB 44.3 KiB
agents/websockets#callablesRpcUrl 12.5 KiB 43.5 KiB
agents/websockets#isCallablesRpcUpgrade 12.4 KiB 43.4 KiB
agents/websockets#WebSockets 17.7 KiB 62.2 KiB
agents/workflows#AgentWorkflow 260.0 KiB 1134.8 KiB
agents/workflows#WorkflowRejectedError 258.7 KiB 1130.2 KiB
agents/x402#normalizeNetwork 14.7 KiB 61.1 KiB
agents/x402#withX402 23.0 KiB 89.2 KiB
agents/x402#withX402Client 104.1 KiB 346.5 KiB

Reported by agent-think[bot].

@pkg-pr-new

pkg-pr-new Bot commented Sep 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@2203

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@2203

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@2203

hono-agents

npm i https://pkg.pr.new/hono-agents@2203

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@2203

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@2203

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@2203

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@2203

commit: 4931795

`pathRowStats` charges a message for the payloads it points at, because
that is what a read materializes. The incremental stats cache did not:
it added the serialized POINTER json, so `stats().totalContentBytes`
depended on whether the cache happened to be warm.

Invalidate instead of tracking. Replicating the base64 charge in append
and update would put the formula in three places and let them drift,
which is how the two disagreed in the first place. A media write now
drops the cache and the next `stats()` derives it — one recursive CTE
read against writes that cost ~1000x more. Text writes, the hot path,
keep the incremental path untouched.

Narrow in practice: Think and pi read `totalContentBytes` from
`getRecentHistory`, which derives from `pathRowStats` on every call and
never from this cache, so no shipped behavior was wrong. But an API that
returns a different number depending on cache warmth is a trap, and the
regression test added here is the first thing that would have hit it.

Claude-Session: https://claude.ai/code/session_01MXpoKqJrUFTqgmDrehDti4
Two findings from auditing Think against the Sessions API.

`SessionStats` loses `totalContentBytes` and `pathLength`. Nothing read
either — Think and pi take their byte total from `getRecentHistory`,
which derives it from `pathRowStats` on every call and never from this
cache. Both fields also measured the ACTIVE BRANCH only, excluding other
branches, other sessions in the object, and the attachment tables, so as
a "how big is this session" signal they answered a different question
than the one anyone would ask them. A real size signal against the 10 GB
ceiling has to sum the tables and deserves its own function. What
remains is the token estimate that gates auto-compaction, which is the
only field with a reader and the reason the cache exists at all.

That also removes the cache invalidation added a commit ago: it existed
solely to keep `totalContentBytes` honest once row stats began charging
attachment bytes. Notably the field that diverged was the unused one —
`tokenEstimate` is stamped from the message BEFORE extraction, so it
always counted the payload and the compaction trigger was never wrong.

`appendMessage` now returns the same inlined message whether it inserted
or found a duplicate. It previously returned `getMessageRaw` on the
duplicate and not-inserted paths, so `AppendResult.message` and the
change feed carried `attachment:sha256:` pointers on some appends and
inline content on others — one call, two shapes, depending on whether
the row already existed.

Think compensated for that with `_messageForCache`, which serialized
every incoming message and substring-searched it for the pointer prefix
before deciding whether to re-read, on the streaming hot path. Fixing
the contract deletes the helper and its three call sites. AIChat's
similarly named helper stays: it does v4 to v5 transformation, which is
genuinely its own concern.

The remaining duplication — both hosts reimplementing the change-feed
cache mirror — is filed as #2205 rather than attempted here.

Claude-Session: https://claude.ai/code/session_01MXpoKqJrUFTqgmDrehDti4
Finishing a fix I got half right. The previous commit made
`appendMessage`'s duplicate paths inline but left the inserted path
returning `prepared.message` — the caller's own object. So a caller that
reads with `attachments: "pointer"` and writes the result back got
pointer form on the insert and inline form on the retry: the same
inconsistency as before, mirrored.

It also invalidated the assumption the previous commit relied on to
delete Think's `_messageForCache`. That helper existed because the feed
could carry pointers; removing it was only safe if Sessions guarantees
it never does. For pointer-form writes, it did not, and the pointers
would have reached Think's live cache and then a model request.

`appendMessage` and `updateMessage` now pass what they return and emit
through `core.inlineMessage()`, so the guarantee holds for every write
regardless of what the caller supplied. It is a no-op by reference when
there are no pointers, so the ordinary write pays a walk and nothing
else.

The invariant now has one choke point instead of being maintained per
branch, which is what went wrong twice here: each fix made another call
site consistent rather than stating the contract in one place.

Both tests fail without the change: insert-then-duplicate returning
identical shapes with nothing pointer-shaped reaching a subscriber, and
the same for updates.

Claude-Session: https://claude.ai/code/session_01MXpoKqJrUFTqgmDrehDti4
…to-a-capability

# Conflicts:
#	packages/ai-chat/src/index.ts
#	packages/ai-chat/src/tests/worker.ts
`updateMessage` resolved attachments before checking the outcome, so a
write against a row that no longer exists materialized every referenced
payload and then returned null — megabytes loaded to be discarded, on a
write guaranteed to fail.

That path is also the one place a pointer legitimately cannot resolve:
if the row is gone its payloads may have been collected with it. Bailing
on `missing` before inlining settles both.

Also drops `MAX_BOUND_PARAMS` and `buildInClauseStrings`, which arrived
with main for the row-size-limit path this branch deletes.

Claude-Session: https://claude.ai/code/session_01MXpoKqJrUFTqgmDrehDti4
Base automatically changed from feat/move-sessions-into-a-capability to main September 3, 2026 14:31

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

3 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

Comment on lines +143 to +145
if (depth > 8) return value;
if (typeof value === "string") return capText(value, limits);
if (Array.isArray(value)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Large tool images become invalid

When a tool returns an inline image above the limit, shapeValue truncates its encoded payload. The model receives a corrupted image.

Prompt for agents
Update packages/agents/src/context/intake.ts so shapeMessage recognizes inline media records before recursively shaping their fields. Data URLs and declared non-text base64 payloads must remain byte-for-byte unchanged, including media nested inside structured tool output. Reuse or align with the media detection rules in packages/agents/src/sessions/attachment-ingest.ts, and add tests for large url and data payloads.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant