fix(experimental): session API better compaction helpers#1210
Conversation
|
/bonk review |
There was a problem hiding this comment.
Good set of defensive fixes. Most are clearly correct. I have two issues — one is a bug introduced by this PR, the other is a correctness concern with the previousSummary extraction logic.
Issues
1. Session.search() return type not updated (bug)
SearchResult.createdAt was made optional in provider.ts, but the inline return type in session.ts:304-309 still declares createdAt: string (required). This means Session.search() now has a type mismatch with what AgentSessionProvider.searchMessages() actually returns (objects without createdAt). This should either be updated to createdAt?: string or the Session.search() method should use the SearchResult type directly instead of an inline type literal.
2. Fix #6 — previousSummary extraction uses wrong id prefix
createCompactFunction (compaction-helpers.ts:459-460) searches for m.id.startsWith("compaction_"), which matches ids created by applyCompactions in agent.ts:337 (compaction_${comp.id}). However, the summary message created by createCompactFunction itself uses a different prefix: compaction-summary-${Date.now()} (line 484).
This means on the second compaction cycle:
- The first compaction produces a
compaction-summary-*message, whichcompact()insession.ts:239-252then stores as aStoredCompactionin the DB. - On the next
getHistory(),applyCompactionsreplaces the compacted range with acompaction_*overlay message containing the stored summary. - So by the time
createCompactFunctionruns again, the message in the history will indeed have acompaction_prefix — but only if the flow goes throughapplyCompactions.
This works in the normal Session.compact() flow, but if createCompactFunction is used standalone (as shown in its JSDoc example), the returned compaction-summary-* messages won't be found by the compaction_ prefix check. The id prefix coupling between these two separate modules is fragile. Consider either:
- Also matching
compaction-summary-prefix, or - Using a more robust signal (e.g., a property on the message) rather than id prefix matching.
Minor observations
- Fixes 1-5 look correct and well-motivated.
- Tests cover the empty prompt edge cases well.
- The
SearchResult.createdAttest (line 543-551) only checks TypeScript type compatibility, which is fine for a type change, but it's a no-op at runtime since any object would pass that check.
| content: string; | ||
| createdAt: string; | ||
| createdAt?: string; | ||
| sessionId?: string; |
There was a problem hiding this comment.
Session.search() at session.ts:308 still has createdAt: string (required), but SearchResult.createdAt is now optional. This is a type mismatch — searchMessages() returns objects without createdAt, but this method signature promises it will always be present. Should be createdAt?: string here too, or better yet, just use the SearchResult type directly.
There was a problem hiding this comment.
Fixed: Session.search() return type now has createdAt?: string matching SearchResult.
| // in the message history (survives DO eviction, unlike closure state) | ||
| const existingCompaction = messages.find((m) => | ||
| m.id.startsWith("compaction_") | ||
| ); |
There was a problem hiding this comment.
This prefix check ("compaction_") only matches overlay messages created by applyCompactions in agent.ts. If createCompactFunction is used outside the Session.compact() flow (as its JSDoc example suggests is possible), the summary messages it produces have id compaction-summary-*, which won't match here. Consider also checking "compaction-summary-":
const existingCompaction = messages.find((m) =>
m.id.startsWith("compaction_") || m.id.startsWith("compaction-summary-")
);There was a problem hiding this comment.
Fixed: Now checks both compaction_ (from applyCompactions overlays) and compaction-summary- (from createCompactFunction standalone output). Works in both the Session.compact() flow and standalone usage.
|
Review posted on PR #1210 with "request changes". Two issues identified:
|
a25d1b1 to
87ad03d
Compare
|
87ad03d to
1c5fdef
Compare
agents
@cloudflare/ai-chat
@cloudflare/codemode
hono-agents
@cloudflare/shell
@cloudflare/think
@cloudflare/voice
@cloudflare/worker-bundler
commit: |
fa74acc to
713d2b4
Compare
f3cdc27 to
b5c545e
Compare
b5c545e to
86c5408
Compare
Edge case fixes:
- freezeSystemPrompt: use !== null so empty prompts are persisted
- Remove double-prefix in compaction summaries
- SearchResult.createdAt now optional (FTS5 can't populate it)
- Session.search() return type matches SearchResult
- appendMessage validates parentId belongs to current session
- Depth guard (10000) in recursive CTEs
- createCompactFunction reads previousSummary from history not closure
Compaction API improvements:
- createCompactFunction returns CompactResult { fromMessageId, toMessageId, summary } instead of UIMessage[]
- Session.compact() uses CompactResult directly — no more prefix searching or message diffing
- Single COMPACTION_PREFIX constant + isCompactionMessage() helper
- onCompaction() builder method registers compaction function
- compactAfter(tokenThreshold) builder method for auto-compaction
- appendMessage auto-compacts when estimated tokens exceed threshold
- Removed needsCompaction(maxMessages) — replaced by token-based approach
Status broadcasting:
- Session broadcasts cf_agent_session events during compaction (phase, tokenEstimate, tokenThreshold)
- Session broadcasts cf_agent_session_error on compaction failure
- MessageType.CF_AGENT_SESSION + CF_AGENT_SESSION_ERROR added to enum
- MessageType exported from agents package
- Auto-compact failure is non-fatal — message still appended
Example updates:
- session-memory uses onCompaction() + compactAfter() builder API
- Streaming chat via @callable({ streaming: true }) + streamText
- Client shows token usage badge (% of threshold or raw estimate)
- Client auto-refreshes messages after compaction overlay appears
- Compaction uses @cf/zai-org/glm-4.7-flash (fast model for summarization)
During iterative compaction, middleMessages could include compaction overlay messages with virtual IDs (compaction_<uuid>). This caused: 1. Virtual IDs stored as toMessageId, breaking future overlays since applyCompactions can't find virtual IDs in the message list 2. The previous summary appearing twice in the LLM prompt — once as PREVIOUS SUMMARY and again as a conversation turn in middleMessages Fix: filter compaction overlay messages from middleMessages before summarization. Use filtered message IDs for fromMessageId/toMessageId. Return null if no real messages remain after filtering.
compact() extends fromMessageId for iterative compactions (using the earliest existing compaction's start), but returned the original CompactResult with the un-extended fromMessageId. The public API now returns the actual persisted fromMessageId.
Set isCompacting immediately on click to disable the button before the server broadcasts the compacting status via WebSocket. Reset on error only — the server broadcast handles the success case.
86c5408 to
1b2f2bd
Compare
|
/bonk review |
There was a problem hiding this comment.
Good set of edge case fixes and a clean refactor of the compaction API. The CompactResult abstraction, COMPACTION_PREFIX unification, and status broadcasting are well-designed. The freezeSystemPrompt null-vs-empty fix, cross-session parentId validation, and CTE depth guards are all clearly correct.
Two issues below — one is a real bug introduced by this PR, the other is a breaking API change that needs attention in downstream consumers.
Issue 1: appendMessage is now async but TestSessionAgent wrapper doesn't await it
Session.appendMessage() was changed from void to async ... Promise<void> (session.ts:223-242). The TestSessionAgent wrapper at tests/agents/session.ts:18-20 still declares appendMessage(): void and doesn't await the call:
appendMessage(message: UIMessage, parentId?: string): void {
this.session.appendMessage(message, parentId); // fire-and-forget
}This means all integration tests calling agent.appendMessage() via RPC silently drop the auto-compaction promise. This won't surface as test failures today because the provider tests don't set a token threshold, but it means:
- Any test that relies on auto-compaction triggering via
appendMessageagainst the realAgentSessionProviderwill be flaky — the compaction runs detached and may not complete before assertions. - If auto-compaction throws (e.g., LLM timeout), the error is swallowed without even the catch handler running in the test context.
The fix is to make the wrapper async and await the result:
async appendMessage(message: UIMessage, parentId?: string): Promise<void> {
await this.session.appendMessage(message, parentId);
}Issue 2: Devin's virtual-ID overlay compaction concern is valid and unmitigated
The Devin review correctly identified that during iterative compaction, createCompactFunction can produce a CompactResult where fromMessageId or toMessageId is a virtual overlay ID (e.g., compaction_<uuid>). The filtering fix at compaction-helpers.ts:479-481 handles the case where overlay messages are filtered out of middleMessages, but doesn't handle the case where the overlay is at compressStart and all real messages are in the tail — in that scenario middleMessages becomes empty and the function correctly returns null.
However, there's a subtler scenario: if there are a few real messages mixed in after the overlay within the compression window, middleMessages will be non-empty and fromMessageId will be a real message. But Session.compact() at session.ts:318-319 then overrides fromId to existing[0].fromMessageId (the first compaction's start), which is always a real message ID. And toMessageId comes from middleMessages[middleMessages.length - 1] which is filtered to real messages. So the stored compaction should always have real IDs.
I think the current code is actually correct for the normal flow, but it would be worth adding a test that exercises iterative compaction with a getHistory() that returns overlay messages to confirm the filtering works end-to-end. The unit tests currently use a stub getHistory that returns a plain message list without overlays.
|
Review posted on PR #1210 with "request changes". Here's a summary of what I found: Issue 1 (bug):
Issue 2 (observation): Virtual overlay ID concern from Devin review is valid but mitigated I traced through the code path carefully. The The previous reviewer comments about |
55ddf5f to
f272c03
Compare
| this._broadcastSessionError( | ||
| err instanceof Error ? err.message : String(err) | ||
| ); | ||
| throw err; |
There was a problem hiding this comment.
compact() broadcasts cf_agent_session_error here and then rethrows. The ChatAgent.compact() wrapper in server.ts:132-138 catches the rethrown error and returns { success: false }. This means the client receives both: (1) the error broadcast via WebSocket, and (2) a successful RPC response with { success: false }.
Consider either not rethrowing after broadcasting the error, or documenting that callers should expect both signals.
| const existing = this.getCompactions(); | ||
| const fromId = | ||
| existing.length > 0 ? existing[0].fromMessageId : removed[0].id; | ||
| this.addCompaction(result.summary, fromId, result.toMessageId); |
There was a problem hiding this comment.
The onCompaction() API accepts arbitrary functions, but compact() stores result.toMessageId without validating it exists in the message history. If a custom compaction function returns a non-existent ID (typo, virtual overlay ID, wrong session), applyCompactions in agent.ts:335 will silently skip the overlay (ids.indexOf returns -1), permanently breaking compaction for that range.
Since the built-in createCompactFunction handles this correctly, this is low priority. But a defensive check here would prevent subtle data corruption from custom implementations:
const historyIds = new Set(history.map(m => m.id));
if (!historyIds.has(result.toMessageId)) {
console.warn(`compact(): toMessageId not found in history, skipping`);
return null;
}|
Review posted on PR #1210 with "request changes". Here's what I found: Issue 1: Issue 2: All previously reported issues from ask-bonk and Devin reviews have been addressed across the 8 commits:
|
…oMessageId 1. compact() no longer rethrows after broadcasting cf_agent_session_error. Callers get null instead of a dual signal (error broadcast + exception). 2. Validate toMessageId exists in history before storing compaction. Prevents silent data corruption from custom compaction functions returning non-existent message IDs.
|
/bonk review |
There was a problem hiding this comment.
Approval
I've reviewed all 14 changed files, traced through the full review history (ask-bonk x3, Devin x5), and run the test suite (799 tests pass, 7 skipped). All previously reported issues have been addressed across the 10 commits. Here's my assessment:
All prior issues resolved
SearchResult.createdAttype mismatch — Fixed:session.ts:398now hascreatedAt?: stringmatchingSearchResult.- Dual prefix check for
previousSummary— Fixed:compaction-helpers.ts:486usesisCompactionMessage()helper, which checksCOMPACTION_PREFIX. Thecompaction-summary-prefix is gone sincecreateCompactFunctionnow returnsCompactResultinstead of creating messages. TestSessionAgent.appendMessagenot async — Fixed:tests/agents/session.ts:18-20properlyawaits the call.- Virtual overlay ID in
toMessageId— Fixed:compaction-helpers.ts:479-481filters overlay messages frommiddleMessages, andsession.ts:316-325validatestoMessageIdexists in history. compact()return valuefromMessageIdmismatch — Fixed:session.ts:346returns{ ...result, fromMessageId: fromId }.- Compact button double-click — Fixed:
client.tsx:295setssetIsCompacting(true)immediately, withfinallyblock at line 302. - Auto-compaction failure propagation — Fixed:
session.ts:235-239wrapscompact()in try/catch (non-fatal). - Falsy check on
_tokenThreshold— Fixed:session.ts:231uses!= null. compact()double-signal (broadcast + rethrow) — Fixed:session.ts:304nowreturn nullinstead of rethrowing, so the client only receives the WebSocket broadcast, not a second error from the RPC layer.- Custom
toMessageIdvalidation — Added:session.ts:316-325checkshistoryIds.has(result.toMessageId)before storing. - Missing changeset — Present at
.changeset/session-api-edge-cases.md, appropriately scoped as patch.
Code quality observations
- The
CompactResultabstraction is a good simplification over the previousUIMessage[]return. It cleanly separates the "what to compact" decision from the "how to store" concern. - The
isCompactionMessage()helper andCOMPACTION_PREFIXconstant eliminate the fragile multi-prefix coupling betweencreateCompactFunctionandapplyCompactions. - The
_broadcastSession/_broadcastSessionErrorpattern with theBroadcasterinterface is well-isolated — the Session class doesn't depend on Agent internals. - Test coverage for the new functionality is thorough: empty prompt persistence,
CompactResultcontract, auto-compaction triggers, broadcast events, and iterative compaction with overlay messages.
Non-blocking suggestions (for follow-up)
-
compact()early return athistory.length < 4doesn't broadcast (session.ts:288). The client'sfinallyblock handles this, but if auto-compaction hits this path, no status is broadcast at all. Consider broadcastingphase: "idle"on this early return too, for consistency. Low priority since auto-compaction callers silently catch errors anyway. -
fromMessageIdis not validated — onlytoMessageIdis checked againsthistoryIds(session.ts:316-325). A custom compaction function could return a non-existentfromMessageId, which would be stored and potentially confuseapplyCompactions. The built-increateCompactFunctionhandles this correctly since it derives IDs from filtered real messages, so this is only relevant for custom implementations.
|
To https://github.com/cloudflare/agents.git |
|
@mattzcarey Bonk workflow failed. Check the logs for details. View workflow run · To retry, trigger Bonk again. |
The summary prompt was structured for coding workflows — it asked for 'file paths, commands run, results obtained' and had a 'Relevant Files' section. When the conversation wasn't about code, the LLM hallucinated file paths and technical artifacts to fill the template. Replaced with a content-agnostic prompt structure (Topic, Key Points, Current State, Open Items) with explicit instruction to only include information that was discussed in the conversation.
| this.addCompaction(result.summary, fromId, result.toMessageId); | ||
| await this.refreshSystemPrompt(); | ||
| return removed.length; | ||
|
|
||
| const tokensAfter = estimateMessageTokens(this.getHistory()); | ||
| this._broadcastSession({ | ||
| phase: "idle", | ||
| tokenEstimate: tokensAfter, | ||
| tokenThreshold: this._tokenThreshold ?? null, | ||
| compacted: { | ||
| tokensBefore, | ||
| tokensAfter | ||
| } | ||
| }); | ||
|
|
||
| return { ...result, fromMessageId: fromId }; |
There was a problem hiding this comment.
🟡 Client stuck in "Compacting..." state if addCompaction or refreshSystemPrompt throws
In Session.compact(), the "compacting" phase is broadcast at line 291, but addCompaction() (line 332) and refreshSystemPrompt() (line 333) are not wrapped in a try/catch. If either throws (e.g., a SQL error), the exception propagates to the caller without broadcasting an "idle" or "error" status. The client, having received the "compacting" broadcast, will remain stuck in the isCompacting=true state indefinitely — the Compact button stays disabled/loading, and the badge shows "Compacting..." with no recovery path.
The compaction function's own errors are correctly handled (lines 298-305 broadcast cf_agent_session_error), but these later steps are unprotected. This is especially relevant for auto-compact via appendMessage (packages/agents/src/experimental/memory/session/session.ts:236), where the error is silently swallowed and no broadcast follows.
Was this helpful? React with 👍 or 👎 to provide feedback.
| const historyIds = new Set(history.map((m) => m.id)); | ||
| if (!historyIds.has(result.toMessageId)) { |
There was a problem hiding this comment.
🟡 toMessageId validation accepts virtual compaction overlay IDs that can never match in applyCompactions
In Session.compact(), the toMessageId validation at line 317-318 builds historyIds from this.getHistory(), which includes compaction overlay messages with virtual IDs (e.g., compaction_<uuid>). If a custom compaction function returns one of these overlay IDs as toMessageId, the validation passes, but applyCompactions in packages/agents/src/experimental/memory/session/providers/agent.ts:335 searches for toMessageId in the raw message path via ids.indexOf(comp.toMessageId) — overlay IDs don't exist there, so endIdx is -1 and the compaction overlay is silently ignored. The built-in createCompactFunction correctly filters these out (packages/agents/src/experimental/memory/utils/compaction-helpers.ts:463), but the validation in Session.compact() should reject overlay IDs to prevent silent failures from custom compaction functions.
Was this helpful? React with 👍 or 👎 to provide feedback.
The history.length < 4 guard was removed from compact() — the minimum message check is now the compaction function's responsibility. Updated the test to use a compaction function that returns null instead of expecting compact() to short-circuit.
The history.length < 4 guard was removed from Session.compact() — the check now lives in createCompactFunction (protectHead + minTailMessages). Added tests to cover both the rejection and acceptance paths.
| onDone: (final: unknown) => { | ||
| const f = final as { message?: UIMessage }; | ||
| if (f.message) { | ||
| // Replace streaming message with final persisted message | ||
| setMessages((prev) => | ||
| prev.map((m) => (m.id === streamId ? f.message! : m)) | ||
| ); | ||
| } |
There was a problem hiding this comment.
🔴 onDone handler silently drops assistant message when no text was streamed
When the LLM response consists only of tool calls (no text output), result.textStream yields nothing, so no text-delta chunks are sent and no streaming message (with id === streamId) is ever created in the messages array. The onDone handler at experimental/session-memory/src/client.tsx:262 uses .map() to replace the streaming message with the final one, but since no message matches streamId, the map returns the array unchanged and the final assistantMsg (containing tool call parts) is never added to the UI.
Trace of the issue
- Server streams no text chunks (tool-call-only response)
- Client
onChunkis never invoked → no streaming message created onDonefires with{ message: assistantMsg }containing tool partsprev.map(m => m.id === streamId ? f.message! : m)matches nothing → array unchanged- Assistant message with tool calls is silently lost from the UI
| onDone: (final: unknown) => { | |
| const f = final as { message?: UIMessage }; | |
| if (f.message) { | |
| // Replace streaming message with final persisted message | |
| setMessages((prev) => | |
| prev.map((m) => (m.id === streamId ? f.message! : m)) | |
| ); | |
| } | |
| onDone: (final: unknown) => { | |
| const f = final as { message?: UIMessage }; | |
| if (f.message) { | |
| // Replace streaming message with final persisted message, | |
| // or append if no streaming message was created (e.g. tool-only responses) | |
| setMessages((prev) => { | |
| const hasStreaming = prev.some((m) => m.id === streamId); | |
| if (hasStreaming) { | |
| return prev.map((m) => (m.id === streamId ? f.message! : m)); | |
| } | |
| return [...prev, f.message!]; | |
| }); | |
| } | |
| }, |
Was this helpful? React with 👍 or 👎 to provide feedback.
| } catch (err) { | ||
| this._broadcastSessionError( | ||
| err instanceof Error ? err.message : String(err) | ||
| ); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
🟡 compact() error path broadcasts "compacting" but never transitions back to "idle"
In session.ts, when _compactionFn throws (lines 305-308), _emitError() is called but _emitStatus("idle") is not. Every other early-return path in compact() (lines 311, 318) correctly emits an "idle" status before returning. The client's onMessage handler in client.tsx:176-178 does handle cf_agent_session_error by setting isCompacting(false), so the primary client recovers. However, other broadcast listeners only tracking the cf_agent_session status type will see "compacting" as the last known phase until the next status-emitting operation occurs (e.g., the next appendMessage call).
Was this helpful? React with 👍 or 👎 to provide feedback.
Added _broadcastTokens() helper that broadcasts cf_agent_session with the current token estimate. Now called from updateMessage, deleteMessages, and clearMessages in addition to appendMessage and compact(). The client always has an up-to-date token count.
…emitError Replace ad-hoc _broadcastSession calls with two focused helpers: - _emitStatus(phase, extra?) — broadcasts phase + token estimate + threshold - _emitError(error) — broadcasts error Removes duplicated broadcast objects throughout compact(). The compacted event now uses tokenEstimate (current) + compacted.tokensBefore instead of redundant tokensBefore/tokensAfter fields.
The summary budget had a fixed minimum of 2000 tokens, which exceeded the entire context window for small compactAfter thresholds (e.g. 1000). Now the budget is capped at 50% of tailTokenBudget, with a minimum of 100 tokens. For the example config (tailTokenBudget=150), the summary budget is ~75 tokens instead of 2000.
Previously minTailMessages overrode tailTokenBudget — when the token budget was small, the token walk gave up quickly but the fallback forced exactly minTailMessages in the tail regardless. With tailTokenBudget=150, the budget was effectively ignored. Now the tail protects whichever is larger: the token-based cut or minTailMessages. The token budget is respected when it protects more messages than the minimum.
| */ | ||
| export function createCompactFunction(opts: CompactOptions) { | ||
| const protectHead = opts.protectHead ?? 2; | ||
| const protectHead = opts.protectHead ?? 3; |
There was a problem hiding this comment.
🟡 protectHead JSDoc says default is 2 but implementation defaults to 3
The CompactOptions.protectHead JSDoc at compaction-helpers.ts:402 says (default: 2), but the implementation at compaction-helpers.ts:438 uses opts.protectHead ?? 3. This was changed in this PR (the old default was 2, matching the docs), but the doc comment was not updated. Users reading the JSDoc will incorrectly expect 2 head messages to be protected, when 3 are actually protected by default.
Was this helpful? React with 👍 or 👎 to provide feedback.
1. Send button becomes a stop button while streaming. Uses a stopped flag to ignore further chunks/done events from the server. 2. Tail budget edge case: if a single message at the tail exceeds the entire tailTokenBudget, it's still included (at least 1 tail message is always protected). Previously this would leave an empty tail. 3. Full README rewrite covering the complete Session API, compaction configuration, context blocks, WebSocket events, and truncation.
Default minTailMessages (4) is too high for a 1000-token window — it forces 4 tail messages regardless of tailTokenBudget. Set to 1 so the token budget is actually respected. Updated README table.
The summary replaces the compressed middle — it doesn't compete with the tail for space. Capping at 50% of tailTokenBudget was wrong for small windows (e.g. 75 tokens for a 150-token tail budget). Now uses the same approach as Hermes: 20% of compressed content with a minimum of 100 tokens, no artificial ceiling.
… README protectHead defaults to 3, minTailMessages now defaults to 2. 4 tail messages was too aggressive for small context windows.
There was a problem hiding this comment.
🟡 computeSummaryBudget JSDoc says "clamped to 2K-8K" but code has no upper clamp and min is 100
The JSDoc at compaction-helpers.ts:278-279 states the budget is "20% of the compressed content, clamped to 2K-8K tokens", but the actual implementation at line 288 is Math.max(100, budget) — a minimum of 100 with no upper bound. This means large histories could produce budgets well above 8K tokens, and the minimum is 100 not 2000. The inline comment at lines 283-286 correctly describes the logic, but the JSDoc contradicts it.
(Refers to lines 278-279)
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Edge case fixes, compaction API improvements, and status broadcasting for the Session API.
Edge case fixes
1. Empty prompt not persisted
freezeSystemPrompt()usedif (stored)which treats empty string as "no value". Fixed:if (stored !== null).2. Double-prefix in compaction summaries
createCompactFunctionandapplyCompactionsboth added prefixes. Fixed: removed prefix fromapplyCompactions— the summary text already has context.3. SearchResult.createdAt always empty
FTS5 can't populate
created_at. Fixed: madecreatedAtoptional onSearchResult. Also fixedSession.search()return type to match.4. Cross-session parentId not validated
appendMessageaccepted anyparentIdwithout checking session ownership. Fixed: validates withSELECT id WHERE session_id = ?, falls back to null.5. No depth guard in recursive CTEs
Both
getHistoryandgetPathLengthCTEs had no depth limit. Fixed:WHERE p.depth < 10000.6. previousSummary closure lost on DO eviction
createCompactFunctionstoredpreviousSummaryin closure. Fixed: extracts from existing compaction messages in history.Compaction API improvements
CompactResultreplacesUIMessage[]createCompactFunctionnow returns{ fromMessageId, toMessageId, summary } | nullinstead of aUIMessage[].Session.compact()uses it directly — no more prefix searching, message diffing, or ID convention coupling.Single
COMPACTION_PREFIXAll compaction messages use
compaction_prefix. Exported asCOMPACTION_PREFIXconstant withisCompactionMessage()helper. No more separate prefixes for overlays vs summaries.onCompaction()+compactAfter()builder methodsappendMessageauto-compacts whenestimateMessageTokens(history) > threshold. Auto-compact failure is non-fatal — the message is still appended.Removed
needsCompaction(maxMessages)Replaced by token-based
compactAfter(tokenThreshold). Message count was a poor proxy for context window usage.Status broadcasting
Session auto-broadcasts to connected WebSocket clients during compaction:
cf_agent_session— status updates{ "type": "cf_agent_session", "phase": "compacting", "tokenEstimate": 1234, "tokenThreshold": 100000 } { "type": "cf_agent_session", "phase": "idle", "tokenEstimate": 456, "tokenThreshold": 100000, "compacted": { "tokensBefore": 1234, "tokensAfter": 456 } }cf_agent_session_error— compaction failures{ "type": "cf_agent_session_error", "error": "504 Gateway Timeout" }Both added to
MessageTypeenum.MessageTypenow exported fromagentspackage.Example updates
session-memoryusesonCompaction()+compactAfter(100)builder API@callable({ streaming: true })+streamText@cf/zai-org/glm-4.7-flash(fast model)cf_agent_session_errorresets compacting stateTest plan
connection-urifailures (unrelated)