Skip to content

fix(terminal): harden the rendering pipeline against dropped bytes, size drift, and flood lag - #221

Merged
khaliqgant merged 9 commits into
mainfrom
fix/terminal-rendering-hardening
Jun 11, 2026
Merged

khaliqgant merged 9 commits into
mainfrom
fix/terminal-rendering-hardening

Conversation

@khaliqgant

Copy link
Copy Markdown
Member

Problem

Recurring terminal rendering corruption: the same line stacking down the screen with stray escape fragments (e.g. Gpasteagain to expand), content drifting to the middle of the screen, and typing lag when many agent messages stream in.

Root causes & fixes

1. The broker silently dropped real PTY bytes (src/main/broker.ts) — the direct cause of the screenshot symptom. The chunk dedup dropped any chunk whose content matched the previous one within 1s, but identical consecutive chunks are normal terminal traffic (repeated keystroke echoes, byte-identical TUI repaint frames). It also deduped by seq alone with a 60s TTL, so a daemon restart (seq reset) could black-hole up to a minute of output. A dropped repaint chunk severs the cursor-positioning escape sequence spanning the gap — the visible G is the tail of a severed ESC[…G, and every Ink repaint then appends instead of overwriting. Dedup now drops only on identity (seq/event_id) + content hash match within a bounded replay window — provably a transport replay — and never otherwise. Suppressed replays get low-noise telemetry.

2. A failed resize was recorded as delivered (fitAndSync + SIGWINCH bounce). Failed resizePty IPCs (or a failed bounce-restore leg, stranding the PTY one row short) were latched by the change-gate, permanently desyncing PTY winsize from the rendered grid — TUIs then position repaints against the wrong row count (the mid-screen drift). New pty-size-sync.ts: a desired/acked model that retries with backoff until the PTY confirms the current size.

3. Every output byte was parsed twice, always. The predictive-echo engine's headless model re-parses all server output even on local sessions where it never engages (needs ≥30ms echo SRTT) — main-thread time stolen from keystroke echo under multi-agent floods. New echo-router.ts: output flows direct to xterm while the engine is dormant; the direct→engine transition reseeds the model from the live screen at a write-queue quiescent point (chunks held during capture land exactly once); >32KB flood frames with no outstanding predictions bypass the engine. Also eliminates the stale-model cursor-injection corruption class.

4. Snapshot replay could double-paint. The post-attach replay baseline was a buffer length, which goes stale across the 10k-chunk trim; the recovery path replayed the entire buffer over the already-painted snapshot. Baselines are now monotonic totals (getPtyChunkTotal / getPtyChunksSinceTotal).

Regression fence (all gating)

  • broker.test.ts: dedup matrix — identical no-identity chunks delivered, daemon seq-reset delivered, delayed replays dropped, per-agent watermarks.
  • echo-router.test.ts: equivalence tests against the real PredictiveEchoEngine + real xterm parser (@xterm/headless) — route transitions, held-chunk windows, flood bypass, reseed aborts must produce a byte-identical screen.
  • pty-size-sync.test.ts / pty-buffer-store.test.ts: the resize and baseline invariants directly.
  • Fidelity suite repaired and promoted to a hard CI gate — it had rotted (web-mock build boots on the Issues tab so all 6 specs timed out; CI had continue-on-error). Now seeds pear-web-initial-tab=agents, selects the spawned agent, and replaces the removed agent-graph flow with an Issues↔Agents cross-view remount mid-stream.
  • redraw-drain now gates on the machine-independent drain invariants (drained, finalFrameRendered); timing metrics stay informational. Current run drains 1,920 repaint frames in 44ms.
  • AGENTS.md updated: never drop a PTY chunk on content or seq alone is now written policy.

Verification

  • npm test 115/115 · npx vitest run 313 passed (+1 pre-existing expected fail)
  • fidelity 6/6 (was 0/6) · redraw 1/1 with new gating asserts
  • typecheck:web clean · typecheck:node identical error set to main (97 pre-existing) · npm run build OK

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jun 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Per-stream PTY dedupe; per-agent integrations delivery with spawn-aware caching and cooldown; renderer adds EchoRouter, PTY size-sync, monotonic PTY replay baselines, runtime wiring, tests, and updated Playwright fidelity gating.

Changes

Broker PTY Deduplication Refactoring

Layer / File(s) Summary
Dedupe constants & hashing
src/main/pty-dedup.ts
Introduce replay-window constants and a chunk-fingerprint function used for identity+content dedupe.
Per-stream dedupe implementation & API
src/main/pty-dedup.ts, src/main/broker.ts
Switch to per-agent-stream identity→hash histories, track seq watermark, add constructor debug option and forgetStream(sessionKey,name), construct deduper with debug flag, and call forgetStream on session teardown.
Dedup/replay edge-case tests
src/main/pty-dedup.test.ts, src/main/broker.test.ts, AGENTS.md
Update tests to require identity+hash for suppression, deliver identity-less chunks repeatedly, add blind-spot logging, seq-reset/watermark/reset cases, per-stream isolation tests, and clarify docs about preferring duplicate delivery over content-only drops.

Integrations System-Message Delivery

Layer / File(s) Summary
Per-agent delivery records & spawn caching
src/main/integrations.ts
Add AgentSystemMessageRecord, per-project/per-agent delivery tracking, lastSubscriptionsReady, cached spawn snippets, and rebroadcast-cooldown constant.
Selective injection & scheduler
src/main/integrations.ts
Rewrite safeInjectSystemMessage to send only to agents lacking the same confirmed text (with spawn-grace), use Promise.allSettled, record fulfilled sends, and advance lastSystemMessageBroadcastAt only on partial/full successes; scheduleSystemMessage supports minDelayMs and cooldown-aware delays.
Tests and IPC spawn recording
src/main/integrations.test.ts, src/main/ipc-handlers.ts
Reset mocks per-test, add helpers and tests for partial retry/new-agent/spawn-suppression/cooldown, and record spawn-embedded delivery in broker:spawn-agent.

Renderer Echo Routing, Size Sync, and Terminal Integration

Layer / File(s) Summary
Predictive-echo status typing
src/renderer/src/lib/predictive-echo.ts
Add PredictiveEchoWithStatus with hasPredictions and update handle typing.
EchoRouter contracts & impl
src/renderer/src/lib/echo-router.ts
New EchoRouter: seed builder, engagement thresholds, reseed capture/timeout/dispose, write ordering, engine/direct routing, rollback on streaming, flood bypass, and input gating.
Echo router tests & IPC knobs
src/renderer/src/lib/echo-router.test.ts, src/renderer/src/lib/ipc-mock.ts
Headless xterm harness and suites validating routing transitions, optimistic predictions, flood bypass and reseed timeouts; IPC mock gains setInputSrtt/setInputEcho knobs and delayed echo behavior.
PTY size sync & tests
src/renderer/src/lib/pty-size-sync.ts, src/renderer/src/lib/pty-size-sync.test.ts
Add PtySizeSync factory with desired/acked state, retry/backoff, noteAcked/invalidateAck/dispose, and comprehensive tests.
PTY buffer monotonic totals
src/renderer/src/stores/pty-buffer-store.ts, tests
Add per-key monotonic totals, increment on flush, export getPtyChunkTotal/getPtyChunksSinceTotal, and add replay-baseline tests.
TerminalRuntime wiring
src/renderer/src/lib/terminal-runtime-registry.ts
Wire runtime to createEchoRouter and createPtySizeSync, track monotonic writtenTotal, route coalesced PTY writes through echoRouter, seed or disable replay on attach, update SIGWINCH bounce to use sizeSync, add noteUserInput and readRuntimeViewportDims, and dispose helpers.
useTerminal optimistic echo & UI/CSP
src/renderer/src/hooks/use-terminal.ts, src/renderer/src/stores/ui-store.ts, src/renderer/index.html
sendInput now calls runtime.noteUserInput(data); web-mock reads pear-web-initial-tab from localStorage; CSP img-src allows pear-avatar:.

Playwright fidelity tests & CI gating

Layer / File(s) Summary
Rendering-corruption spec & fidelity match
tests/playwright/rendering-corruption.spec.ts, playwright.fidelity.config.ts
Add deterministic rendering-corruption fidelity test and expand testMatch to include it alongside fidelity-no-duplication.
Fidelity harness initial tab & redraw gating
tests/playwright/fidelity-no-duplication.spec.ts, tests/playwright/redraw-drain.spec.ts, tests/playwright/REDRAW-DRAIN-RESULTS.md, .github/workflows/ci.yml
Ensure fidelity harness boots to Agents tab, strengthen redraw-drain gating invariants (drained and finalFrameRendered), update docs and CI step comments.

Sequence Diagram

sequenceDiagram
  participant Broker as Broker (worker_stream)
  participant BrokerManager as BrokerManager
  participant Runtime as TerminalRuntime
  participant EchoRouter as EchoRouter
  participant Engine as PredictiveEchoEngine
  participant Xterm as Xterm
  participant User as User

  Broker->>BrokerManager: PTY chunk (event_id/seq + bytes)
  BrokerManager->>Runtime: emit PTY-chunk batch
  Runtime->>EchoRouter: onServerOutput(coalesced bytes)
  alt EchoRouter route == engine and Engine.hasPredictions
    EchoRouter->>Engine: onServerOutput(bytes)
    Engine->>Xterm: onEngineOutput(predicted/confirmed writes)
  else
    EchoRouter->>Xterm: direct write(bytes)
  end

  User->>Runtime: sendInput(data) -> noteUserInput
  Runtime->>EchoRouter: onUserInput(data)
  alt getInputSrtt() >= PREDICTION_ENGAGE_THRESHOLD_MS
    EchoRouter->>Engine: reseed() / onUserInput -> engage predictions
  else
    EchoRouter->>Xterm: optimistic local echo (no engine)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hop through bytes and hashes bright,
I guard each chunk by day and night,
I seed the echo, route with care,
I nudge the size and clear the air,
Terminals hum — the stream’s alright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: hardening the rendering pipeline against dropped bytes, size drift, and typing lag. It is specific, concise, and directly related to the primary objectives of the PR.
Description check ✅ Passed The description is comprehensive and directly related to the changeset. It clearly explains the problem, root causes, fixes, regression fences, and verification steps, all of which align with the actual changes in the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/terminal-rendering-hardening

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.

@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
.github/workflows/ci.yml (1)

57-60: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Stale comment references removed flag.

Line 58 mentions "traces from the continue-on-error fidelity step," but continue-on-error: true was removed from the fidelity step at line 53. The always() condition still makes sense (to capture traces from any failed step), but the comment should be updated to reflect the current behavior.

📝 Suggested comment update
       - name: Upload Playwright traces
-        # always() so traces from the continue-on-error fidelity step are captured too
-        # (a continue-on-error failure does not trip failure()). No-op when nothing failed.
+        # always() so traces are captured from any failed step. No-op when nothing failed.
         if: always()
🤖 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 @.github/workflows/ci.yml around lines 57 - 60, Update the inline comment
above the "Upload Playwright traces" step so it no longer mentions "the
continue-on-error fidelity step"; instead state that the if: always() condition
ensures Playwright traces are uploaded for any failed step (or when failures
occur), and that it is a no-op when there are no traces to upload; modify the
comment text near the Upload Playwright traces step and the if: always() line to
reflect this current behavior.
src/main/broker.ts (1)

2625-2632: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Session teardown still leaves stale PTY dedupe entries.

Line 2626 cleans up per-agent, but session-level teardown removes agents without clearing ptyStreamDedupe for that session key. This can retain stale dedupe state and grow memory over time.

Suggested fix
+  private clearPtyDedupeForSession(sessionKey: string): void {
+    const prefix = `${sessionKey}:`
+    for (const key of Array.from(this.ptyStreamDedupe.keys())) {
+      if (key.startsWith(prefix)) this.ptyStreamDedupe.delete(key)
+    }
+  }
+
   private dropSession(sessionKey: string, options: { disconnectOnly: boolean }): void {
     this.closeInputStreamsForSession(sessionKey)
+    this.clearPtyDedupeForSession(sessionKey)
     this.clearBrokerTimeoutCountsForProject(projectIdFromSessionKey(sessionKey))
🤖 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/main/broker.ts` around lines 2625 - 2632, forgetAgentSession currently
deletes only the single ptyStreamDedupe entry for `${sessionKey}:${name}` but
when the last agent for a session is removed (sessionKeys.size === 0) it deletes
the agent map without clearing other ptyStreamDedupe entries tied to that
session, leaving stale entries. Update forgetAgentSession (or add a small helper
called from it) so that when sessionKeys.size === 0 you also remove all
ptyStreamDedupe entries whose keys startWith `${sessionKey}:` (or otherwise
clear the session-scoped dedupe entries), using the existing ptyStreamDedupe and
agentSessions symbols to locate and clean those entries before deleting the
agentSessions entry.
🧹 Nitpick comments (1)
src/main/broker.test.ts (1)

1472-1564: ⚡ Quick win

Add one regression for event_id/id identity paths.

The new matrix validates seq and no-identity behavior well, but dedupe precedence also uses event_id/id. Add a case for same event_id+same bytes (suppressed) and same event_id+different bytes (delivered) to lock this branch.

As per coding guidelines, "Prefer stable event identity over content matching. PTY and broker events should carry event_id, id, or seq; dedupe by identity AND content hash together."

🤖 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/main/broker.test.ts` around lines 1472 - 1564, Add a regression test in
broker.test.ts that exercises the event identity paths: create a BrokerManager,
mock window via createMockWindow and startLocalWithWindow, grab the localized
listener (from local.onEvent.mock.calls.at(-1)?[0]) and send two pairs of
worker_stream events using the same event_id (or id) — first pair with identical
chunk bytes (expect only one 'broker:pty-chunk' delivery), second pair with
different chunk bytes (expect both deliveries). Use the same PROJECT_ID and
agent name(s) as other tests, filter win.webContents.send calls by
'broker:pty-chunk' and assert the expected deliveries to ensure dedupe honors
event_id/id plus content-hash precedence.

Source: Coding guidelines

🤖 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/main/broker.ts`:
- Around line 2505-2508: The branch that returns false on missing identity
currently drops telemetry; modify the check around the identity variable (the
"if (!identity) return false" branch) to increment a low-volume telemetry
counter (e.g., pty_chunks.missing_identity) and emit a debug-gated or sampled
log message before returning false so missing-identity events are visible
without flooding; use the existing debug/logger and metrics utilities in the
module (or add a sampled logger helper) and ensure the telemetry is lightweight
and only triggers under the debug flag or with probabilistic sampling.

In `@src/renderer/src/lib/echo-router.ts`:
- Around line 135-137: In reseedPending, when the engine has changed (the check
deps.getEngine() !== engine) the current code discards heldChunks by setting
heldChunks = []; instead, preserve and immediately release them to avoid losing
live PTY output: detect the engine mismatch in reseedPending and call the
existing release mechanism (e.g., the function or method that normally
flushes/dispatches heldChunks) for each chunk or the batch before clearing;
reference the variables and functions heldChunks, reseedPending,
deps.getEngine(), engine and the release/dispatch routine so the code falls back
to direct release rather than dropping the buffered data.

---

Outside diff comments:
In @.github/workflows/ci.yml:
- Around line 57-60: Update the inline comment above the "Upload Playwright
traces" step so it no longer mentions "the continue-on-error fidelity step";
instead state that the if: always() condition ensures Playwright traces are
uploaded for any failed step (or when failures occur), and that it is a no-op
when there are no traces to upload; modify the comment text near the Upload
Playwright traces step and the if: always() line to reflect this current
behavior.

In `@src/main/broker.ts`:
- Around line 2625-2632: forgetAgentSession currently deletes only the single
ptyStreamDedupe entry for `${sessionKey}:${name}` but when the last agent for a
session is removed (sessionKeys.size === 0) it deletes the agent map without
clearing other ptyStreamDedupe entries tied to that session, leaving stale
entries. Update forgetAgentSession (or add a small helper called from it) so
that when sessionKeys.size === 0 you also remove all ptyStreamDedupe entries
whose keys startWith `${sessionKey}:` (or otherwise clear the session-scoped
dedupe entries), using the existing ptyStreamDedupe and agentSessions symbols to
locate and clean those entries before deleting the agentSessions entry.

---

Nitpick comments:
In `@src/main/broker.test.ts`:
- Around line 1472-1564: Add a regression test in broker.test.ts that exercises
the event identity paths: create a BrokerManager, mock window via
createMockWindow and startLocalWithWindow, grab the localized listener (from
local.onEvent.mock.calls.at(-1)?[0]) and send two pairs of worker_stream events
using the same event_id (or id) — first pair with identical chunk bytes (expect
only one 'broker:pty-chunk' delivery), second pair with different chunk bytes
(expect both deliveries). Use the same PROJECT_ID and agent name(s) as other
tests, filter win.webContents.send calls by 'broker:pty-chunk' and assert the
expected deliveries to ensure dedupe honors event_id/id plus content-hash
precedence.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d9f489f8-de78-4303-be88-081438cadc0d

📥 Commits

Reviewing files that changed from the base of the PR and between f98c6bb and 4bf2bc7.

📒 Files selected for processing (17)
  • .github/workflows/ci.yml
  • AGENTS.md
  • src/main/broker.test.ts
  • src/main/broker.ts
  • src/renderer/src/hooks/use-terminal.ts
  • src/renderer/src/lib/echo-router.test.ts
  • src/renderer/src/lib/echo-router.ts
  • src/renderer/src/lib/predictive-echo.ts
  • src/renderer/src/lib/pty-size-sync.test.ts
  • src/renderer/src/lib/pty-size-sync.ts
  • src/renderer/src/lib/terminal-runtime-registry.ts
  • src/renderer/src/stores/pty-buffer-store.test.ts
  • src/renderer/src/stores/pty-buffer-store.ts
  • src/renderer/src/stores/ui-store.ts
  • tests/playwright/REDRAW-DRAIN-RESULTS.md
  • tests/playwright/fidelity-no-duplication.spec.ts
  • tests/playwright/redraw-drain.spec.ts

Comment thread src/main/broker.ts Outdated
Comment thread src/renderer/src/lib/echo-router.ts Outdated
@agent-relay-code

Copy link
Copy Markdown
Contributor

Implemented the fixes I validated in the current checkout.

Changes
Fixed stale/unsafe PR issues in:

Addressed Comments

  • CodeRabbit: stale Playwright trace-upload comment still mentioned the removed continue-on-error fidelity step. Fixed in .github/workflows/ci.yml.
  • CodeRabbit: missing-identity PTY chunks had no low-noise telemetry. Fixed with a counter and debug-gated power-of-two logging in src/main/broker.ts and src/main/broker.ts.
  • CodeRabbit: echo-router dropped held PTY output when the predictive engine changed during reseed capture. Fixed to release held bytes directly in src/renderer/src/lib/echo-router.ts, with regression coverage in src/renderer/src/lib/echo-router.test.ts.
  • CodeRabbit: session teardown could leave stale PTY dedupe state. Fixed with session-scoped cleanup in src/main/broker.ts called from src/main/broker.ts, with regression coverage in src/main/broker.test.ts.
  • CodeRabbit: requested event identity regression coverage for event_id/id. Added in src/main/broker.test.ts.
  • CodeRabbit: docstring coverage warning. Left unchanged; it is a repository-wide quality warning unrelated to this terminal-rendering PR.
  • CodeRabbit: merge-conflict checklist item. No code change; current checkout has no unresolved conflict markers.

Advisory Notes
None.

Validation
Passed locally:

  • npm ci
  • npm run verify:mcp-resources-drift
  • npm run typecheck:web
  • npm test
  • npx vitest run
  • npm run build
  • npm run build:web
  • npx playwright install --with-deps chromium
  • npx playwright test --config playwright.fidelity.config.ts
  • npx playwright test --config playwright.redraw.config.ts

Also ran npm run typecheck:node; it still fails with the repo’s existing non-blocking node type errors, consistent with the workflow’s continue-on-error: true note. I could not verify remote GitHub check status or mergeability from the local harness, so I’m not marking this READY.

khaliqgant added a commit that referenced this pull request Jun 10, 2026
…e swap, log identity-less PTY streams

- echo-router: if the engine changes while a reseed capture is in flight,
  release the held chunks directly instead of dropping them (unreachable
  today outside dispose, but dropping live PTY bytes is the exact corruption
  class this PR kills — defend in depth).
- broker: log once per stream (not per chunk) when PTY chunks arrive with no
  seq/event_id, per the AGENTS.md low-noise telemetry guideline — replay
  dedup is blind on such streams and that should be visible.

Both behaviors pinned with tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@agent-relay-code

Copy link
Copy Markdown
Contributor

Reviewed PR #221 against .workforce/pr.diff, changed files, current checkout, and visible CodeRabbit review comments. I did not make code edits because every actionable review item was already handled in the current checkout.

Addressed comments

  • coderabbitai[bot]: CI trace upload comment referenced removed continue-on-error; stale, already fixed in .github/workflows/ci.yml:58.
  • coderabbitai[bot]: session teardown could leave stale PTY dedupe entries; stale, already handled by clearPtyDedupeForSession in src/main/broker.ts:2652 and called from dropSession at src/main/broker.ts:4233.
  • coderabbitai[bot]: missing-identity PTY telemetry was dropped; stale, current code logs once per stream in src/main/broker.ts:2512.
  • coderabbitai[bot]: echo-router engine mismatch dropped held chunks; stale, current code releases held output directly in src/renderer/src/lib/echo-router.ts:139.
  • coderabbitai[bot]: add event_id/id dedupe regression; stale, already covered in src/main/broker.test.ts:1585.

Verification

Passed locally:

  • npm ci
  • npm run verify:mcp-resources-drift
  • npm run typecheck:web
  • npm test
  • npx vitest run
  • focused Vitest suite for broker/router/size-sync/buffer tests
  • npm run build
  • npm run build:web
  • npx playwright install --with-deps chromium
  • npx playwright test --config playwright.fidelity.config.ts
  • npx playwright test --config playwright.redraw.config.ts

Notes:

  • npm run typecheck:node still fails with the documented pre-existing main-process type debt; CI marks that step continue-on-error.
  • I did not run the macOS packaged-mcp-smoke lane in this Linux sandbox.
  • GitHub API currently reports the PR as mergeable: false, mergeable_state: dirty. I found no conflict markers in the checkout and did not make speculative merge edits, so I am not marking this READY.

@agent-relay-code

Copy link
Copy Markdown
Contributor

Implemented the still-valid fixes from the current checkout.

Changes made:

  • Updated stale Playwright trace upload comment in .github/workflows/ci.yml:58.
  • Added session-scoped PTY dedupe cleanup in src/main/broker.ts:2652, called from src/main/broker.ts:4230.
  • Added broker regressions for event_id/id dedupe and session-drop dedupe cleanup in src/main/broker.test.ts:1603 and src/main/broker.test.ts:1626.

Addressed comments

  • CodeRabbit: trace upload comment still mentioned removed continue-on-error; fixed in .github/workflows/ci.yml:58.
  • CodeRabbit: missing-identity PTY telemetry; stale, current code already logs once per stream in src/main/broker.ts:2512.
  • CodeRabbit: echo-router engine mismatch could drop held chunks; stale, current code already releases held chunks in src/renderer/src/lib/echo-router.ts:139, covered at src/renderer/src/lib/echo-router.test.ts:362.
  • CodeRabbit: session teardown could leave stale PTY dedupe state; fixed in src/main/broker.ts:2652 and src/main/broker.ts:4230, with regression at src/main/broker.test.ts:1626.
  • CodeRabbit: requested event_id/id PTY dedupe regression; fixed in src/main/broker.test.ts:1603.
  • CodeRabbit: docstring coverage warning; not changed because it is repo-wide and unrelated to this terminal-rendering PR.
  • CodeRabbit: merge-conflict checklist item; no conflict markers found in the current checkout.
  • agent-relay-code[bot]: prior summary said all actionable items were already fixed; stale for this checkout, revalidated and fixed the remaining trace-comment/session-cleanup/event-id-test gaps above.

Advisory Notes

  • The docstring coverage warning is a repo-wide quality gate outside this PR’s terminal rendering scope.

Verification

Passed:

  • npm ci
  • npm run verify:mcp-resources-drift
  • npm run typecheck:web
  • npm test
  • npx vitest run
  • npx vitest run src/main/broker.test.ts
  • npm run build
  • npm run build:web
  • npx playwright install --with-deps chromium
  • npx playwright test --config playwright.fidelity.config.ts
  • npx playwright test --config playwright.redraw.config.ts

npm run typecheck:node still fails with the repo’s documented pre-existing non-blocking node type errors. I did not verify remote GitHub check status or mergeability, so I am not marking this READY.

@agent-relay-code

Copy link
Copy Markdown
Contributor

Implemented and verified fixes for the current checkout.

Changes made:

  • Fixed stale Playwright trace-upload comment in .github/workflows/ci.yml.
  • Added session-wide PTY dedupe cleanup on broker session drop.
  • Added broker regressions for event_id/id identity+hash dedupe and session-drop cleanup.
  • Tightened echo-router streaming behavior: active stream chunks bypass predictive echo, held chunks during reseed cancel the transition and render directly, and pre-armed server output drops back to direct.
  • Adjusted the rendering-corruption Playwright harness so prediction reconciliation is exercised in the final quiet-prompt phase instead of injecting artificial echo bytes into the repaint storm.

Addressed comments

  • CodeRabbit: trace upload comment still referenced continue-on-error; fixed in .github/workflows/ci.yml:58.
  • CodeRabbit: missing-identity PTY telemetry; already valid in current code at src/main/broker.ts:2512.
  • CodeRabbit: echo-router could drop held chunks when engine changed during reseed; current code releases directly at src/renderer/src/lib/echo-router.ts:201, with coverage at src/renderer/src/lib/echo-router.test.ts:253.
  • CodeRabbit: session teardown could leave stale PTY dedupe state; fixed in src/main/broker.ts:2652 and called from src/main/broker.ts:4231, with coverage at src/main/broker.test.ts:1626.
  • CodeRabbit: requested event_id/id PTY dedupe regression; fixed in src/main/broker.test.ts:1579.
  • CodeRabbit: docstring coverage warning; not changed because it is repo-wide and outside this terminal-rendering PR scope.
  • CodeRabbit: merge-conflict checklist item; no conflict markers found in the current checkout.
  • agent-relay-code[bot]: prior summaries were stale/incomplete for this checkout; revalidated and fixed the remaining trace-comment, session-cleanup, event-id coverage, and Playwright fidelity failures above.

Advisory Notes

  • The docstring coverage warning is a repository-wide quality gate outside this PR’s scope.
  • I could not run the macOS-only packaged-mcp-smoke lane in this Linux environment.

Verification

Passed:

  • npm ci
  • npm run verify:mcp-resources-drift
  • npm run typecheck:web
  • npm test
  • npx vitest run
  • focused Vitest suites for broker/router/size-sync/buffer
  • npm run build
  • npm run build:web
  • npx playwright install --with-deps chromium
  • npx playwright test --config playwright.fidelity.config.ts
  • npx playwright test --config playwright.redraw.config.ts

npm run typecheck:node still fails with the repo’s documented pre-existing non-blocking node type errors. I did not verify remote GitHub mergeability/check state, so I am not marking this READY.

khaliqgant and others added 5 commits June 11, 2026 09:53
…ize drift, and flood lag

Four root-cause fixes for the recurring rendering corruption (stacked
duplicate lines with stray escape fragments, content drifting mid-screen,
typing lag under multi-agent output), each with a gating regression fence:

- broker PTY dedup: drop a chunk ONLY on identity (seq/event_id) + content
  hash match within a bounded replay window. Content-only dedup dropped
  legitimate identical chunks (repeated keystroke echoes, byte-identical TUI
  repaint frames) and seq-only TTL dedup dropped fresh output for up to 60s
  after a daemon seq reset — dropped repaint bytes sever escape sequences
  and make every Ink repaint stack instead of overwrite.
- PTY size sync: a resize only counts as delivered when the resizePty IPC
  resolves (desired/acked model with retrying backoff in pty-size-sync.ts).
  Failed resizes were recorded as sent, permanently desyncing PTY winsize
  from the grid — TUIs then repaint against the wrong row count.
- echo routing (echo-router.ts): server output flows direct to xterm while
  the predictive-echo engine is dormant, killing the always-on headless
  model double-parse (the typing-lag hot path) and the stale-model cursor
  injection corruption class; the direct→engine transition reseeds the
  model from the live screen at a write-queue quiescent point, and >32KB
  flood frames with no predictions bypass the engine.
- replay baselines: monotonic chunk totals (getPtyChunkTotal /
  getPtyChunksSinceTotal) replace buffer-length baselines that went stale
  across the 10k trim and replayed the whole buffer over a painted snapshot.

Regression fence: broker dedup matrix (replay/seq-reset/no-identity),
echo-router equivalence tests against the REAL engine + REAL xterm parser,
pty-size-sync + buffer-baseline invariant tests, the fidelity playwright
suite repaired (web-mock initial-tab override + agent selection; obsolete
agent-graph flow replaced with an Issues↔Agents remount) and promoted to a
hard CI gate, and redraw-drain now gating on drained/finalFrameRendered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e swap, log identity-less PTY streams

- echo-router: if the engine changes while a reseed capture is in flight,
  release the held chunks directly instead of dropping them (unreachable
  today outside dispose, but dropping live PTY bytes is the exact corruption
  class this PR kills — defend in depth).
- broker: log once per stream (not per chunk) when PTY chunks arrive with no
  seq/event_id, per the AGENTS.md low-noise telemetry guideline — replay
  dedup is blind on such streams and that should be visible.

Both behaviors pinned with tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…+ stream-aware prediction gating, with a self-driving storm repro

The post-fix corruption report (character-interleaved rows, stacked panel
repaints, stray typed glyphs inside repainted rows) reproduced in a new
Playwright storm harness and bisected to two renderer defects:

- Write-order inversion: a flood-bypass route flip wrote directly to xterm
  while earlier chunks were still queued in the predictive-echo engine's
  async tail — bytes landed out of order, shearing escape sequences and
  overlaying row text. The router now has a single ordering domain: any
  write issued while engine work is queued chains behind it; the chain
  collapses back to synchronous direct writes once drained (zero overhead
  in the common local case). Reproduced by a unit test with a slowed model
  (failed before, passes after).

- Predictions during active streaming: mosh-style optimistic glyphs are a
  quiet-prompt tool. While a TUI streams in-place repaints, the engine
  erases/re-renders its glyphs around EVERY frame; any model/live drift
  then erases real rows and strands glyphs inside repainted panels — the
  exact live symptoms. Streaming-sized chunks (>1KB) now arm a quiet
  window: outstanding predictions are rolled back at still-valid positions
  the moment streaming starts, and no new predictions render until the
  stream has been quiet for 300ms. Keystroke echo never trips the window.

New rendering-corruption.spec.ts drives the real renderer through an
Ink-style storm (integration-dump text, randomly split repaint frames,
>32KB flood chunks, typing mid-flood with the engine engaged via new mock
setInputSrtt/setInputEcho knobs, idle-prompt typing at the end) and gates
on byte-faithfulness: the live viewport must equal a reference
@xterm/headless render of the canonical chunk stream. Runs under the
fidelity Playwright config in CI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…agent sessions

Agents were receiving the same <integrations-update> setup context several
times and acknowledging each copy — the duplicated-transcript reports
("Ready. I'll follow…" ×4, "Setup context received" ×2). Three compounding
vectors, all in the broadcast machinery:

- All-or-nothing retry: the delivered-signature was recorded only when
  EVERY agent's send succeeded (Promise.all), so one flaky send re-broadcast
  the identical update to every agent on the next reconcile. Delivery is
  now recorded per agent (allSettled); retries reach only the agents that
  missed it.
- Spawn double-delivery: a new agent received the snippet inside its spawn
  task AND again from the next project broadcast (the guard was
  project-level). The spawn handler now records the embedded snippet as
  already-delivered for the new agent.
- Staged text churn: scope-label/channel-name hydration and subscription
  readiness converge in stages, each slightly changing the rendered text
  and re-broadcasting the full update mid-task. Broadcasts now honor a 60s
  cooldown; later changes replace the pending timer so the trailing
  broadcast carries the latest text once.

Per-agent records also mean a newly arrived agent gets the current update
without re-sending it to everyone else. Tests cover partial-failure retry,
new-agent-only delivery, spawn-skip, cooldown hold-and-release, and the
unchanged-update no-op (now hermetic against background cloud hydration).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ts; fix(avatar): allow pear-avatar: in img-src CSP

Residual codex double-acknowledgment: a freshly spawned agent received the
integrations snippet in its spawn task, then the next project broadcast
rendered a slightly different text (spawn hardcoded subscriptionsReady=true;
paths still hydrating) and delivered a near-identical second setup message.

- Spawn records now carry fromSpawnAt; broadcasts whose text differs are
  skipped for that agent during a 60s grace window, with a follow-up
  scheduled after the grace so a real post-spawn change still lands once.
- Spawn snippets render with the last reconciled subscription readiness
  instead of a hardcoded true, removing the main text-mismatch source.
- Low-noise send telemetry: every integrations-update send logs one line
  (agent, project, size) so duplicate-transcript reports are diagnosable.

Avatar: the renderer CSP (img-src 'self' data: https:) blocked the custom
pear-avatar: protocol that serves cached avatars, so the cache built to
survive expiring provider URLs was never usable — once the token-scoped
Google avatar URL went stale the avatar broke. Allowlist pear-avatar:.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@khaliqgant
khaliqgant force-pushed the fix/terminal-rendering-hardening branch from e46af01 to 6bd8394 Compare June 11, 2026 07:55

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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/main/broker.ts`:
- Around line 2501-2502: The class BrokerManager contains references to
this.ptyStreamDedupe and this.ptyStreamsMissingIdentity which are undefined and
break compilation; replace these stale references with the new deduper API
(this.ptyDeduper) used elsewhere: remove uses of this.ptyStreamDedupe and
this.ptyStreamsMissingIdentity and call the appropriate methods on
this.ptyDeduper (e.g., delete/remove by key or use its provided dedupe cleanup
method) using the same sessionKey:name composite key, ensuring the cleanup logic
matches existing ptyDeduper semantics so BrokerManager no longer references
non-existent properties.

In `@tests/playwright/fidelity-no-duplication.spec.ts`:
- Around line 337-340: Update the stale test comment so it accurately describes
the executed flow: instead of saying the test "Leave the Agents view entirely
(Issues tab) and come back", change it to state that the test toggles the Agents
view layout (split/tabs) mid-stream to ensure remounting within Agents does not
replay duplicate buffer content; reference the existing comment block near the
fidelity-no-duplication test that mentions "Agents view" and the split/tabs
toggles to locate and replace the wording.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cb27411a-c3a9-462e-9e4b-cfbc45fce3ab

📥 Commits

Reviewing files that changed from the base of the PR and between b05179c and 6bd8394.

📒 Files selected for processing (24)
  • .github/workflows/ci.yml
  • AGENTS.md
  • playwright.fidelity.config.ts
  • src/main/broker.test.ts
  • src/main/broker.ts
  • src/main/integrations.test.ts
  • src/main/integrations.ts
  • src/main/ipc-handlers.ts
  • src/renderer/index.html
  • src/renderer/src/hooks/use-terminal.ts
  • src/renderer/src/lib/echo-router.test.ts
  • src/renderer/src/lib/echo-router.ts
  • src/renderer/src/lib/ipc-mock.ts
  • src/renderer/src/lib/predictive-echo.ts
  • src/renderer/src/lib/pty-size-sync.test.ts
  • src/renderer/src/lib/pty-size-sync.ts
  • src/renderer/src/lib/terminal-runtime-registry.ts
  • src/renderer/src/stores/pty-buffer-store.test.ts
  • src/renderer/src/stores/pty-buffer-store.ts
  • src/renderer/src/stores/ui-store.ts
  • tests/playwright/REDRAW-DRAIN-RESULTS.md
  • tests/playwright/fidelity-no-duplication.spec.ts
  • tests/playwright/redraw-drain.spec.ts
  • tests/playwright/rendering-corruption.spec.ts
✅ Files skipped from review due to trivial changes (3)
  • src/renderer/index.html
  • AGENTS.md
  • tests/playwright/REDRAW-DRAIN-RESULTS.md
🚧 Files skipped from review as they are similar to previous changes (16)
  • src/renderer/src/lib/predictive-echo.ts
  • playwright.fidelity.config.ts
  • src/main/ipc-handlers.ts
  • src/renderer/src/hooks/use-terminal.ts
  • src/renderer/src/stores/ui-store.ts
  • src/renderer/src/lib/ipc-mock.ts
  • tests/playwright/rendering-corruption.spec.ts
  • src/renderer/src/lib/pty-size-sync.ts
  • src/renderer/src/lib/pty-size-sync.test.ts
  • src/renderer/src/stores/pty-buffer-store.ts
  • tests/playwright/redraw-drain.spec.ts
  • src/renderer/src/stores/pty-buffer-store.test.ts
  • src/main/integrations.ts
  • src/renderer/src/lib/echo-router.test.ts
  • src/renderer/src/lib/echo-router.ts
  • src/renderer/src/lib/terminal-runtime-registry.ts

Comment thread src/main/broker.ts Outdated
Comment on lines +337 to +340
// Leave the Agents view entirely (Issues tab) and come back mid-stream —
// the cross-view unmount/remount that used to replay duplicate buffer
// content on top of the parked runtime. (The old 'agent graph' layout
// this test used no longer exists.)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update stale remount test comment to match executed flow.

Lines 337-340 describe leaving Agents for Issues, but Lines 342-346 only toggle split/tabs within Agents. The mismatch makes the regression intent harder to trust during future triage.

Suggested wording fix
-    // Leave the Agents view entirely (Issues tab) and come back mid-stream —
-    // the cross-view unmount/remount that used to replay duplicate buffer
-    // content on top of the parked runtime. (The old 'agent graph' layout
-    // this test used no longer exists.)
+    // Toggle terminal layout mid-stream (split -> tabs) to exercise the
+    // runtime remount path that previously replayed duplicate buffered content.
+    // (The old 'agent graph' layout this test used no longer exists.)
🤖 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 `@tests/playwright/fidelity-no-duplication.spec.ts` around lines 337 - 340,
Update the stale test comment so it accurately describes the executed flow:
instead of saying the test "Leave the Agents view entirely (Issues tab) and come
back", change it to state that the test toggles the Agents view layout
(split/tabs) mid-stream to ensure remounting within Agents does not replay
duplicate buffer content; reference the existing comment block near the
fidelity-no-duplication test that mentions "Agents view" and the split/tabs
toggles to locate and replace the wording.

khaliqgant and others added 4 commits June 11, 2026 10:03
- broker.ts: drop PtyStreamDedupeState, ptyChunkHash, getBrokerEventName,
  getBrokerEventFrom, resolveBrokerConnectionPath (all superseded by
  PtyChunkDeduper/BrokerEventHistory extraction); revive promise to const
- integrations.ts: let integrations → const (never reassigned)
- ipc-handlers.ts: drop assertDirectory import (unused)
- relay-workspace.ts: let pendingHandle → const (ref is post-await only)
- pty-buffer-store.ts: drop stale no-console eslint-disable directive

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…roker.ts typecheck rebase artifacts

The CI storm repro caught a real corruption window the slow runner hits:
the predictive-echo engine places optimistic glyphs by reading its
confirmed model's cursor synchronously, but while router ops are queued a
server chunk has already reached the live terminal whose model parse is
still backlogged — the model cursor is on a stale ROW. A prediction
placed then renders on the wrong row, and the engine's cleanup
(erasePredictionRegion) skips erasing glyphs whose row no longer matches
its prediction row, stranding them on screen (the stacked-frame-remnant /
displaced-echo class: stale FRAME headers above the panel, typed echo
landing rows below it).

Fix: the router only hands keystrokes to the engine when its op chain is
empty (queuedOps === 0). Skipping costs only the optimistic glyph for
that keystroke; the byte still reaches the PTY and echoes back.

- echo-router.test.ts: writeDelayMs hook now models a PARSE backlog
  (delay before the model terminal consumes the bytes) instead of a
  delayed resolution, which is what a busy renderer actually looks like;
  new regression test strands a prediction across a row move and fails
  without the gate.
- rendering-corruption.spec.ts: STORM_CPU=<n> bisect knob applies CDP CPU
  throttling to reproduce the slow-CI timing locally (1/4 fail at 4x
  pre-fix, 14/14 pass at 6x post-fix).
- broker.ts: drop leftover references to the removed PTY content-dedup
  fields and silence the use-before-assign false positive in
  reviveSession (typecheck:node CI failures from the rebase).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rebase onto main resolved the dedup conflict by keeping main's
extracted PtyChunkDeduper (content-alone 1s window + identity-alone 60s
window) and dropping this branch's hardened implementation — exactly the
semantics this branch exists to kill (content-alone drops lose real PTY
bytes; identity-alone drops eat fresh output after a daemon seq reset).
The branch's broker.test.ts invariants caught it.

Port the hardened logic (recovered from pre-rebase fe4a19d) into the
extracted class shape:
- drop ONLY on identity (event_id/seq) AND content-hash match within a
  512-chunk per-stream window
- identity-less chunks always deliver, with a once-per-stream blind-spot
  log
- same identity + different bytes, or seq at/below the watermark outside
  the window → daemon restart: reset the window and deliver
- forgetStream() per-stream cleanup wired back into forgetAgentSession

pty-dedup.test.ts rewritten to the hardened invariants (the old suite
asserted the corrupting behavior).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@khaliqgant
khaliqgant merged commit b2bcf4f into main Jun 11, 2026
4 of 5 checks passed
@khaliqgant
khaliqgant deleted the fix/terminal-rendering-hardening branch June 11, 2026 08:57
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