Skip to content

fix: strip blockContent from tool results to prevent O(n²) token growth#241

Closed
1shCha wants to merge 1 commit intomainfrom
julie_issue
Closed

fix: strip blockContent from tool results to prevent O(n²) token growth#241
1shCha wants to merge 1 commit intomainfrom
julie_issue

Conversation

@1shCha
Copy link
Collaborator

@1shCha 1shCha commented Mar 16, 2026

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

  • Refactor
    • Enhanced event filtering in AI workspace operations. The system now intelligently filters certain event types when constructing AI tool results, ensuring cleaner and more focused results while maintaining complete data persistence. This improvement increases consistency and reliability across workspace operations.

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>
@vercel
Copy link
Contributor

vercel bot commented Mar 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
thinkex Ready Ready Preview, Comment Mar 16, 2026 7:53pm

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Mar 16, 2026

📝 Walkthrough

Walkthrough

A new internal helper function eventForToolResult is introduced to filter workspace events, selectively omitting ITEM_UPDATED events containing blockContent from tool results while preserving all other events. This filtering is applied across multiple workspace operation paths without affecting database persistence behavior.

Changes

Cohort / File(s) Summary
Event Filtering for Tool Results
src/lib/ai/workers/workspace-worker.ts
Adds eventForToolResult() helper function to conditionally filter ITEM_UPDATED events with blockContent from tool result propagation. Updated multiple call sites in workspace event paths (ITEM_CREATED, BULK_ITEMS_CREATED, ITEM_UPDATED, DELETE, and various dashboard update handlers) to use the new filtering function instead of direct event references.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A helper function hops with care,
Filtering events through the air,
Some blockContent must not pass through,
Tool results get a cleaner view,
While database keeps the full delight,
Events flow now, oh so right! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: a fix that strips blockContent from tool results to prevent token count explosion during serial edits.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch julie_issue
📝 Coding Plan
  • Generate coding plan for human review comments

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can enable review details to help with troubleshooting, context usage and more.

Enable the reviews.review_details setting to include review details such as the model used, the time taken for each step and more in the review comments.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 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 any cast 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

📥 Commits

Reviewing files that changed from the base of the PR and between 79bd7be and 4d40993.

📒 Files selected for processing (1)
  • src/lib/ai/workers/workspace-worker.ts

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

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;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 16, 2026

Choose a reason for hiding this comment

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

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>
Fix with Cubic

@urjitc urjitc closed this Mar 18, 2026
@github-project-automation github-project-automation bot moved this from Backlog to Done in Dev Board Mar 18, 2026
@urjitc urjitc deleted the julie_issue branch March 18, 2026 22:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants