fix: strip blockContent from tool results to prevent O(n²) token growth#241
fix: strip blockContent from tool results to prevent O(n²) token growth#241
Conversation
When the AI makes serial note edits, each ITEM_UPDATED event was carrying the full BlockNote JSON (blockContent) in the tool result. This entered the AI SDK conversation history and was re-sent on every subsequent step, causing token counts to compound multiplicatively — the root cause of the $9.40 / 3.74M token spike incidents. Add eventForToolResult() which returns undefined for ITEM_UPDATED events that contain blockContent, and the full event otherwise. Returning undefined causes useOptimisticToolUpdate to skip the cache patch, allowing the Supabase broadcast to deliver the full event without hitting the dedup guard. ITEM_CREATED events pass through unchanged (one-time cost, needed for immediate UI rendering). Applied at all 11 return sites in workspace-worker.ts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughA new internal helper function Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment Tip You can enable review details to help with troubleshooting, context usage and more.Enable the |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/lib/ai/workers/workspace-worker.ts (1)
23-47: Well-documented helper with sound design.The function correctly filters ITEM_UPDATED events with blockContent while preserving all other event types. The comment block thoroughly explains the rationale (token cost) and UI behavior (optimistic cache skip for proper Supabase delivery).
One optional improvement: the
as anycast on line 39 bypasses type safety. Consider a narrower approach:♻️ Optional: improve type safety
function eventForToolResult(event: WorkspaceEvent): WorkspaceEvent | undefined { if (event.type === "ITEM_UPDATED") { - const data = (event.payload.changes as any)?.data; - if (!data || !("blockContent" in data)) return event; + const data = event.payload.changes?.data; + if (data && typeof data === "object" && "blockContent" in data) { + return undefined; + } return undefined; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/ai/workers/workspace-worker.ts` around lines 23 - 47, The helper eventForToolResult uses a blunt cast (event.payload.changes as any) which bypasses type safety; replace the cast with a narrow type guard or safe access pattern: define or inline a type guard that checks event.payload?.changes has a data property (e.g., function isChangesWithData(x): x is { data: unknown }) then use that guard before checking "blockContent" in data, or use a typed intermediate like const changes = event.payload?.changes as Partial<{ data?: Record<string, unknown> }> and then test typeof/ 'in' on changes.data; update references to WorkspaceEvent/ITEM_UPDATED, payload.changes and blockContent in eventForToolResult so the compiler can verify the property access without using as any.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/lib/ai/workers/workspace-worker.ts`:
- Around line 23-47: The helper eventForToolResult uses a blunt cast
(event.payload.changes as any) which bypasses type safety; replace the cast with
a narrow type guard or safe access pattern: define or inline a type guard that
checks event.payload?.changes has a data property (e.g., function
isChangesWithData(x): x is { data: unknown }) then use that guard before
checking "blockContent" in data, or use a typed intermediate like const changes
= event.payload?.changes as Partial<{ data?: Record<string, unknown> }> and then
test typeof/ 'in' on changes.data; update references to
WorkspaceEvent/ITEM_UPDATED, payload.changes and blockContent in
eventForToolResult so the compiler can verify the property access without using
as any.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9697ec09-dd41-4491-920e-f99583c7bf08
📒 Files selected for processing (1)
src/lib/ai/workers/workspace-worker.ts
There was a problem hiding this comment.
1 issue found across 1 file
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/lib/ai/workers/workspace-worker.ts">
<violation number="1" location="src/lib/ai/workers/workspace-worker.ts:41">
P1: Dropping the update event here prevents the initiating client from seeing AI note edits until a refetch, because neither the optimistic hook nor realtime will apply a replacement event.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| if (event.type === "ITEM_UPDATED") { | ||
| const data = (event.payload.changes as any)?.data; | ||
| if (!data || !("blockContent" in data)) return event; | ||
| return undefined; |
There was a problem hiding this comment.
P1: Dropping the update event here prevents the initiating client from seeing AI note edits until a refetch, because neither the optimistic hook nor realtime will apply a replacement event.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib/ai/workers/workspace-worker.ts, line 41:
<comment>Dropping the update event here prevents the initiating client from seeing AI note edits until a refetch, because neither the optimistic hook nor realtime will apply a replacement event.</comment>
<file context>
@@ -20,6 +20,32 @@ import { getNoteContentAsMarkdown } from "@/lib/utils/format-workspace-context";
+ if (event.type === "ITEM_UPDATED") {
+ const data = (event.payload.changes as any)?.data;
+ if (!data || !("blockContent" in data)) return event;
+ return undefined;
+ }
+ // ITEM_CREATED: keep the full event so the optimistic cache shows the new note
</file context>
When the AI makes serial note edits, each ITEM_UPDATED event was carrying the full BlockNote JSON (blockContent) in the tool result. This entered the AI SDK conversation history and was re-sent on every subsequent step, causing token counts to compound multiplicatively — the root cause of the $9.40 / 3.74M token spike incidents.
Add eventForToolResult() which returns undefined for ITEM_UPDATED events that contain blockContent, and the full event otherwise. Returning undefined causes useOptimisticToolUpdate to skip the cache patch, allowing the Supabase broadcast to deliver the full event without hitting the dedup guard. ITEM_CREATED events pass through unchanged (one-time cost, needed for immediate UI rendering).
Applied at all 11 return sites in workspace-worker.ts.
Summary by CodeRabbit