Skip to content

fix(webview): throttle state pushes to prevent gray screen OOM - #1077

Closed
JunyongParkDev wants to merge 4 commits into
Zoo-Code-Org:mainfrom
JunyongParkDev:fix/webview-state-throttling
Closed

fix(webview): throttle state pushes to prevent gray screen OOM#1077
JunyongParkDev wants to merge 4 commits into
Zoo-Code-Org:mainfrom
JunyongParkDev:fix/webview-state-throttling

Conversation

@JunyongParkDev

@JunyongParkDev JunyongParkDev commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Related GitHub Issue

Closes: #629

Description

This PR reduces full-state webview churn during long-running tasks with large message histories. The hot paths previously serialized and posted the complete message array for every message addition and queue-state change, producing repeated multi-megabyte snapshots at the message count reported in #629.

  • Adds a provider-scoped 500 ms leading/trailing debounce with a 1 second maximum wait for full state updates that omit task history.
  • Routes new-message additions and message-queue state changes through the throttled path while keeping task start, API boundaries, and stream completion immediate.
  • Flushes pending state before the first lightweight update for a newly created partial message and before task abort, and cancels pending work when the provider is disposed.
  • Logs rejected throttled state schedules and flushes without interrupting message persistence or task-abort cleanup.
  • Clears aggregated task costs when the active task changes and ignores delayed cost responses for inactive tasks.

Partial streaming continues to use the existing lightweight messageUpdated path. The change coalesces high-frequency full snapshots without changing the webview message protocol or task lifecycle. Reviewers should pay particular attention to full-state/lightweight-message ordering and the rejection paths around persistence and abort cleanup.

Test Procedure

Run the focused regression tests:

pnpm --dir src exec vitest run \
  core/webview/__tests__/ClineProvider.spec.ts \
  core/task/__tests__/Task.spec.ts \
  core/task/__tests__/Task.throttle.test.ts

pnpm --dir webview-ui exec vitest run \
  src/components/chat/__tests__/ChatView.spec.tsx

Run the repository validation commands:

pnpm test
pnpm check-types
pnpm lint

Local results:

  • Extension regression tests: 3 files and 220 tests passed.
  • ChatView regression tests: 1 file and 26 tests passed.
  • Full webview UI suite: 142 files and 1,559 tests passed.
  • Full repository test run: 10 test tasks passed.
  • Type checks: 11 package tasks passed.
  • Lint: 11 package tasks passed.

The regression tests cover:

  • Leading/trailing coalescing and the 1 second maximum wait.
  • Explicit flush behavior, provider disposal, and scheduling/flush rejection handling.
  • Throttled message and message-queue updates.
  • Ordering between a new partial message and subsequent messageUpdated delivery.
  • Immediate clean-state delivery at task start.
  • Final-state delivery and cleanup continuity when task abort encounters a state-push failure.
  • Message persistence continuity when throttled state scheduling fails.
  • Aggregated-cost cleanup on task switch and rejection of delayed responses from an inactive task.

Manual stress verification:

  1. Load 3,525 messages of approximately 2 KB each into an actual VS Code webview.
  2. Exercise rapid full-state update requests against a state payload of approximately 7.22 MB.
  3. Confirm that burst updates are coalesced and the renderer remains responsive to an extension/webview round trip.

Pre-Submission Checklist

  • Issue Linked: This PR is linked to an approved GitHub Issue (see "Related GitHub Issue" above).
  • Scope: My changes are focused on the linked issue (one major feature/fix per PR).
  • Self-Review: I have performed a thorough self-review of my code.
  • Testing: New and/or updated tests have been added to cover my changes (if applicable).
  • Visual Snapshot (UI changes only): Not applicable; this is a behavior-only change with no static visual changes.
  • Documentation Impact: I have considered if my changes require documentation updates (see "Documentation Updates" section below).
  • Contribution Guidelines: I have read and agree to the Contributor Guidelines.

Visual Snapshots

Not applicable. This change does not alter static visual output.

Videos (interaction / animation only)

Not applicable. This change does not add or alter a visible interaction or animation.

Documentation Updates

  • No documentation updates are required.
  • Yes, documentation updates are required.

Additional Notes

This PR intentionally addresses the frequency of large full-state pushes. Redesigning message delivery to eliminate the O(N) payload itself is outside the scope of #629.

The manual stress run verified webview renderer survival and responsiveness at the reported message count. It did not record an absolute V8 heap measurement.

Get in Touch

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Task state pushes now use throttled provider methods with explicit flushes for partial messages, unanswered asks, and aborts. Provider disposal cancels pending updates. ChatView now resets and renders aggregated costs by current task.

Changes

Task webview state and cost handling

Layer / File(s) Summary
Throttled provider state publishing
src/core/webview/ClineProvider.ts, src/core/webview/__tests__/ClineProvider.spec.ts
Added leading and trailing throttled state posting with a maximum wait, explicit flushing, failure logging, and disposal cancellation.
Task state update integration
src/core/task/Task.ts, src/core/task/__tests__/Task.spec.ts, src/core/task/__tests__/Task.throttle.test.ts
Message-queue and message-addition updates use throttled posting. Partial messages, unanswered asks, task startup, and task abortion coordinate pending state delivery. Tests cover ordering and rejection handling.
Current-task cost rendering
webview-ui/src/components/chat/ChatView.tsx, webview-ui/src/components/chat/__tests__/ChatView.spec.tsx
Aggregated costs reset and apply only to the current task. Task headers render the current task’s cost entry. Tests cover task switching and delayed responses.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Task
  participant ClineProvider
  participant Webview
  participant ChatView

  Task->>ClineProvider: schedule throttled task state
  ClineProvider->>Webview: post coalesced state
  Webview->>ChatView: deliver task state and aggregated-cost events
  ChatView->>ChatView: accept data for currentTaskId
  Task->>ClineProvider: flush pending state before partial update or abort event
  ClineProvider->>Webview: post pending state
Loading

Possibly related issues

  • Zoo-Code issue 371 — Covers task-scoped webview state handling in Task and ChatView.

Possibly related PRs

Suggested labels: awaiting-review

Suggested reviewers: navedmerchant

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies throttled webview state pushes as the primary fix for gray-screen OOM failures.
Description check ✅ Passed The description covers the issue, implementation, testing, checklist, documentation impact, and scope with sufficient detail.
Linked Issues check ✅ Passed The changes satisfy issue #629 by throttling full-state pushes, preserving immediate updates, flushing abort state, clearing costs, and adding regression coverage.
Out of Scope Changes check ✅ Passed All changes support issue #629, including throttling, lifecycle flushes, disposal handling, cost cleanup, and related regression tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Coalesce repeated full-state webview updates with a leading and trailing debounce and a one-second maximum wait. Keep task-start, API-boundary, and stream-completion updates immediate, and flush pending state during partial-message initialization and task abort.

Reset aggregated task costs when switching tasks and ignore delayed responses for inactive tasks so stale per-task data is not retained or displayed.

Add regression coverage for debounce timing, flush and disposal behavior, partial-message ordering, queue failures, and task-switch cost cleanup.

Signed-off-by: JunyongParkDev <jun94.park@samsung.com>
Verify stale webview messages are cleared and posted through the immediate state path before the first task message. This keeps task startup outside the throttled update path.

Signed-off-by: JunyongParkDev <jun94.park@samsung.com>
Ensure unanswered ask state reaches the webview before Message listeners can respond. Keep already answered asks on the throttled path and cover both ordering cases with regression tests.

Signed-off-by: JunyongParkDev <jun94.park@samsung.com>
Exercise the stringification branch for non-Error rejections so both debounced state-post failure paths are covered.

Signed-off-by: JunyongParkDev <jun94.park@samsung.com>
@JunyongParkDev
JunyongParkDev force-pushed the fix/webview-state-throttling branch from 8156406 to 11be8e5 Compare July 31, 2026 16:27

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/task/__tests__/Task.spec.ts`:
- Around line 1698-1734: Add rejection-path tests for addToClineMessages and
abortTask, using rejected throttled provider calls to verify
RooCodeEventName.Message and RooCodeEventName.TaskAborted behavior remains
observable as appropriate. Cover disposal’s flush path as needed, asserting
errors are logged or otherwise handled and that the rejections do not escape or
prevent completion of the surrounding operation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5a4ab539-0185-4b98-be94-9cd2849185a2

📥 Commits

Reviewing files that changed from the base of the PR and between 8156406 and 11be8e5.

📒 Files selected for processing (7)
  • src/core/task/Task.ts
  • src/core/task/__tests__/Task.spec.ts
  • src/core/task/__tests__/Task.throttle.test.ts
  • src/core/webview/ClineProvider.ts
  • src/core/webview/__tests__/ClineProvider.spec.ts
  • webview-ui/src/components/chat/ChatView.tsx
  • webview-ui/src/components/chat/__tests__/ChatView.spec.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/core/task/tests/Task.throttle.test.ts
  • src/core/task/Task.ts
  • src/core/webview/tests/ClineProvider.spec.ts
  • webview-ui/src/components/chat/tests/ChatView.spec.tsx
  • src/core/webview/ClineProvider.ts
  • webview-ui/src/components/chat/ChatView.tsx

Comment on lines +1698 to +1734
it("waits for an unanswered ask flush before emitting the message", async () => {
const task = new Task({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task",
startTask: false,
})
const taskAccess = getTaskTestAccess(task)
vi.spyOn(taskAccess, "saveClineMessages").mockResolvedValue(true)
let releaseFlush!: () => void
const pendingFlush = new Promise<void>((resolve) => {
releaseFlush = resolve
})
const flushSpy = vi.mocked(mockProvider.flushPostStateToWebviewThrottled).mockReturnValueOnce(pendingFlush)
const messageListener = vi.fn()
task.on(RooCodeEventName.Message, messageListener)
const message = {
ts: 1,
type: "ask" as const,
ask: "resume_task" as const,
}

const addPromise = taskAccess.addToClineMessages(message)

await Promise.resolve()
expect(mockProvider.postStateToWebviewThrottled).toHaveBeenCalledOnce()
expect(mockProvider.postStateToWebviewThrottled).toHaveBeenCalledWith()
expect(flushSpy).toHaveBeenCalledOnce()
expect(flushSpy).toHaveBeenCalledWith()
expect(messageListener).not.toHaveBeenCalled()

releaseFlush()
await addPromise

expect(flushSpy.mock.invocationCallOrder[0]).toBeLessThan(messageListener.mock.invocationCallOrder[0])
expect(messageListener).toHaveBeenCalledWith({ action: "created", message })
})

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
git ls-files | rg '(^|/)Task\.ts$|(^|/)Task\.spec\.ts$' || true

echo "== Task.spec relevant ranges =="
for f in $(git ls-files | rg 'Task\.spec\.ts$'); do
  echo "--- $f lines 1650-1900"
  sed -n '1650,1900p' "$f" | nl -ba -v1650
done

echo "== Task.ts outline/search around candidate symbols =="
for f in $(git ls-files | rg '^(src/core/task)/Task\.ts$|Task\.ts$'); do
  echo "--- $f"
  wc -l "$f"
  ast-grep outline "$f" --match addToClineMessages --view expanded || true
  ast-grep outline "$f" --match abortTask --view expanded || true
  ast-grep outline "$f" --match messageQueueStateChangedHandler --view expanded || true
  echo "--- postState/flush occurrences"
  rg -n "postStateToWebviewThrottled|flushPostStateToWebviewThrottled|messageQueueStateChangedHandler|TaskAborted|dispose\(" "$f" -C 4
done

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Task.spec relevant ranges =="
for f in $(git ls-files | rg 'Task\.spec\.ts$'); do
  echo "--- $f lines 1650-1900"
  sed -n '1650,1900p' "$f" | cat -n
done

echo "== Task.ts candidate occurrences =="
f="src/core/task/Task.ts"
echo "--- $f line count"
wc -l "$f"
echo "--- postState/flush/messageQueue occurrences"
grep -n "postStateToWebviewThrottled\|flushPostStateToWebviewThrottled\|messageQueueStateChangedHandler\|TaskAborted\|dispose(" "$f" -C 8 || true

echo "== addTask/messageQueue abort-like handlers in Task.ts =="
# Use Python to print concise named function blocks if possible without heavy runtime.
python3 - <<'PY'
from pathlib import Path
p=Path('src/core/task/Task.ts')
text=p.read_text()
for needle in ['addToClineMessages', 'messageQueueStateChangedHandler', 'abortTask']:
    print(f'---- {needle} ----')
    i=text.find(needle)
    while i!=-1:
        start=max(0,i-800)
        end=min(len(text),i+2200)
        print(text[start:end])
        print('----')
        i=text.find(needle,end)
PY

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 46233


Add rejection-path coverage for the throttled provider calls.

addToClineMessages awaits postStateToWebviewThrottled() before emitting RooCodeEventName.Message. If flushPostStateToWebviewThrottled() rejects, execution stops before this.emit(RooCodeEventName.Message, ...) and saveClineMessages(). abortTask awaits flushPostStateToWebviewThrottled() before emitting RooCodeEventName.TaskAborted, and disposal only wraps dispose()/saveClineMessages(), so a rejection can prevent post-state flush completion and expose an unwrapped rejection before abort completion.

Add tests in this block, and a rejection-path test near the abort tests, that assert the logged or observable behavior and do not let these rejections propagate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/task/__tests__/Task.spec.ts` around lines 1698 - 1734, Add
rejection-path tests for addToClineMessages and abortTask, using rejected
throttled provider calls to verify RooCodeEventName.Message and
RooCodeEventName.TaskAborted behavior remains observable as appropriate. Cover
disposal’s flush path as needed, asserting errors are logged or otherwise
handled and that the rejections do not escape or prevent completion of the
surrounding operation.

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.

fix(webview): throttle state pushes to prevent gray screen OOM at high message counts

1 participant