Skip to content

fix(terminal): keep reconciler live through cursor queries (#421)#422

Merged
khaliqgant merged 2 commits into
mainfrom
fix/codex-421-render-overlap
Jul 19, 2026
Merged

fix(terminal): keep reconciler live through cursor queries (#421)#422
khaliqgant merged 2 commits into
mainfrom
fix/codex-421-render-overlap

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Jul 18, 2026

Copy link
Copy Markdown
Member

Summary

  • keep the terminal convergence detector/repair loop live when Claude emits idle DEC-private cursor-position queries
  • classify only one-or-more complete exact ESC[?6n sequences as reconciliation-neutral; mixed, partial, non-private, and empty output remains activity
  • continue delivering every PTY byte through echo accounting, the echo router, and xterm
  • add a real-CLI turn-boundary-follow-up workload that mirrors the reported long markdown turn followed immediately by a short path question
  • emit the raw-stream discriminator for clean checkpoints, not only failures

Root cause

Current Claude emits an exact five-byte ESC[?6n query roughly every 200 ms while its screen is idle. The sequence does not paint, erase, scroll, resize, or move the cursor, but terminal-runtime-registry.writeChunks previously reset lastOutputAt and incremented activitySerial for every batch. That permanently closed the reconciler quiet gate, silently disabling Pear's only always-on divergence detector and repair backstop. A renderer divergence created by another transient path could therefore preserve stale prior-turn rows indefinitely.

The production screenshot's ✻ Crunched for … line also matches Claude's UI. The installed inline Codex CLI does not retain that duration line after completion.

Real-harness evidence

The new workload prints a 60-line markdown preview containing a remembered absolute path, waits for the CLI's completed-turn prompt, then submits the short path follow-up with no delay.

  • Claude: bufferType=alternate viewport=[0,0] cursorMatch=true rawBytes=128622 rawVsBroker=0 rawVsRenderer=0
  • Codex: bufferType=normal viewport=[69,69] cursorMatch=true rawBytes=251998 rawVsBroker=0 rawVsRenderer=0

Both focused real-Electron cases passed without reconciler divergence telemetry.

Validation

  • npm run lint
  • npm run typecheck
  • npm run build
  • npm run test:all (129 Node tests + 609 Vitest tests)
  • npm run verify:mcp-resources-drift
  • TERM_FIDELITY_SKIP_BUILD=1 TERM_FIDELITY_WORKLOAD=turn-boundary-follow-up npm run test:term-fidelity -- --cli=claude
  • TERM_FIDELITY_SKIP_BUILD=1 TERM_FIDELITY_WORKLOAD=turn-boundary-follow-up npm run test:term-fidelity -- --cli=codex

Fixes #421

@khaliqgant khaliqgant added the no-agent-relay-review Disable agent-relay automated PR review/fixes label Jul 18, 2026
@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 commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR excludes query-only cursor-position responses from reconciler visual-activity clocks, adds focused runtime tests, separates raw and visual terminal-fidelity metrics, records byte-level checkpoint evidence, and adds a turn-boundary follow-up workload.

Changes

Terminal activity and fidelity

Layer / File(s) Summary
Neutral query quiet-gate handling
src/renderer/src/lib/terminal-runtime-registry.ts, src/renderer/src/lib/terminal-reconciler.ts, src/renderer/src/lib/terminal-runtime-registry.dom.test.ts
Query-only ESC[?6n output remains written to xterm but no longer advances reconciler activity; tests cover mixed output, repeated queries, and quiet-state transitions.
Visual activity and checkpoint evidence
tests/term-fidelity/harness.ts, tests/term-fidelity/oracle.ts
The fidelity probe tracks visual activity separately from raw output, quiet checks use visual counters, and checkpoints compare replayed bytes with broker and renderer grids.
Turn-boundary regression workload
tests/term-fidelity/workloads.ts, tests/term-fidelity/oracle.ts, tests/term-fidelity/README.md
A workload submits a completed markdown-heavy turn, waits for a fresh prompt, sends a full-path follow-up, and checkpoints the result.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant PTY
  participant TerminalRuntimeRegistry
  participant xterm
  participant TerminalReconciler
  participant FidelityOracle
  PTY->>TerminalRuntimeRegistry: deliver query or screen-mutating output
  TerminalRuntimeRegistry->>xterm: write combined PTY output
  TerminalRuntimeRegistry->>TerminalReconciler: advance activity for visual output only
  TerminalRuntimeRegistry->>FidelityOracle: expose raw and visual activity
  FidelityOracle->>TerminalReconciler: evaluate quiet checkpoint
Loading

Poem

A bunny watched the cursor query fly,
While painted screens went hopping by.
Quiet gates opened, grids aligned,
Fresh follow-ups left stale text behind.
Byte by byte, the burrow cheers—
Clean terminal paths for all our peers!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% 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 change: keeping the terminal reconciler active through cursor-query output.
Description check ✅ Passed The description is directly related to the PR and accurately describes the fix, regression workload, and validation.
Linked Issues check ✅ Passed The PR addresses #421 by reproducing both CLIs, adding the turn-boundary workload, and fixing quiet-gate handling for cursor queries.
Out of Scope Changes check ✅ Passed The added tests, workload, and docs all support the terminal-query reconciliation fix and do not appear unrelated.
✨ 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/codex-421-render-overlap

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.

@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!

@cursor

cursor Bot commented Jul 19, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@khaliqgant
khaliqgant merged commit b52004a into main Jul 19, 2026
4 of 5 checks passed
@khaliqgant
khaliqgant deleted the fix/codex-421-render-overlap branch July 19, 2026 05:11

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4773b6479c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +293 to +299
const hasFreshPrompt = harness.cli === 'codex'
// Codex renders a rotating placeholder after the prompt glyph; the
// literal text changes between launches (for example "Implement
// {feature}" or "Find and fix a bug in @filename").
? /^\s*›\s+\S.*$/mu.test(afterMarker)
: /^\s*❯[\s\u00a0]*$/mu.test(afterMarker)
return hasFreshPrompt && (harness.cli !== 'claude' || hasDuration)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Recognize completion prompts for every supported CLI

For OpenCode and Grok matrix runs, this condition falls into the non-Codex branch and waits only for Claude’s exact prompt line. SUPPORTED_CLIS includes both of those CLIs (tests/term-fidelity/harness.ts:8), and runCanonicalWorkloads invokes this new workload unconditionally, so their canonical runs will spend the full five-minute timeout here and fail before submitting the follow-up. Add CLI-specific completion detection for those renderers, or limit this workload to the CLIs whose prompt protocol it recognizes.

Useful? React with 👍 / 👎.

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
tests/term-fidelity/README.md (1)

34-34: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale workload count: "all six" should be "all seven".

This PR brings the matrix to 7 workloads (see the renumbered list below and the 7-entry array in workloads.ts), but this line still says six.

📝 Suggested fix
-`TERM_FIDELITY_WORKLOAD=<workload-directory-name>` to execute one canonical
-workload. Normal matrix and CI runs must leave it unset so all six execute.
+`TERM_FIDELITY_WORKLOAD=<workload-directory-name>` to execute one canonical
+workload. Normal matrix and CI runs must leave it unset so all seven execute.
🤖 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/term-fidelity/README.md` at line 34, Update the workload-count wording
in the README from “all six” to “all seven” to match the seven workloads defined
by the matrix and the workloads array.
🧹 Nitpick comments (1)
tests/term-fidelity/oracle.ts (1)

592-618: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Duplicate raw-stream replay/diff work on failing checkpoints.

This new diagnostic block recomputes getRawStreamreplayRawToGridbrokerCellGridcountCellDiffs (×2), and the existing divergence branch below (!quiet.reached || differingCells > 0) recomputes the exact same values independently. For any failing checkpoint this now replays the full raw byte stream through a fresh headless terminal and diffs the grid twice.

♻️ Suggested refactor: compute once, reuse in the divergence branch
-  try {
-    const raw = await getRawStream(harness.page, agentName)
-    const rawReplay = await replayRawToGrid(raw, broker.rows, broker.cols)
-    const brokerCells = await brokerCellGrid(broker)
-    const rawVsBroker = countCellDiffs(rawReplay.cells, brokerCells, broker.rows, broker.cols)
-    const rawVsRenderer = countCellDiffs(rawReplay.cells, renderer.cells, broker.rows, broker.cols)
-    const cursorMatch = ...
-    console.log(...)
-  } catch (error) {
-    console.warn(...)
-  }
+  let rawEvidence: { raw: string; rawReplay: Awaited<ReturnType<typeof replayRawToGrid>>; brokerCells: Cell[][] } | null = null
+  try {
+    const raw = await getRawStream(harness.page, agentName)
+    const rawReplay = await replayRawToGrid(raw, broker.rows, broker.cols)
+    const brokerCells = await brokerCellGrid(broker)
+    rawEvidence = { raw, rawReplay, brokerCells }
+    const rawVsBroker = countCellDiffs(rawReplay.cells, brokerCells, broker.rows, broker.cols)
+    const rawVsRenderer = countCellDiffs(rawReplay.cells, renderer.cells, broker.rows, broker.cols)
+    // ...cursorMatch/console.log unchanged
+  } catch (error) {
+    console.warn(...)
+  }

Then thread rawEvidence into the divergence branch below instead of re-fetching/re-replaying.

🤖 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/term-fidelity/oracle.ts` around lines 592 - 618, Refactor the
checkpoint diagnostics so the raw-stream evidence is computed once and reused.
Store the results of getRawStream, replayRawToGrid, brokerCellGrid, both
countCellDiffs comparisons, and cursor matching in a rawEvidence value, then
pass or reuse it in the existing !quiet.reached || differingCells > 0 divergence
branch instead of recomputing them there. Preserve the current clean-checkpoint
logging and divergence artifact behavior.
🤖 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/renderer/src/lib/terminal-runtime-registry.dom.test.ts`:
- Around line 247-340: Add a regression test covering replay or duplicate
delivery through the terminal runtime catch-up path, such as re-attachment or
post-snapshot replay, rather than only the live subscribePtyBuffer path. Verify
that replayed query-only batches preserve the reconciler activity serial and
quiet state, while replayed mixed query-and-paint batches increment the serial
and close the quiet gate. Anchor the test to writeChunks and the existing
runtime/reconciler harness symbols.

---

Outside diff comments:
In `@tests/term-fidelity/README.md`:
- Line 34: Update the workload-count wording in the README from “all six” to
“all seven” to match the seven workloads defined by the matrix and the workloads
array.

---

Nitpick comments:
In `@tests/term-fidelity/oracle.ts`:
- Around line 592-618: Refactor the checkpoint diagnostics so the raw-stream
evidence is computed once and reused. Store the results of getRawStream,
replayRawToGrid, brokerCellGrid, both countCellDiffs comparisons, and cursor
matching in a rawEvidence value, then pass or reuse it in the existing
!quiet.reached || differingCells > 0 divergence branch instead of recomputing
them there. Preserve the current clean-checkpoint logging and divergence
artifact behavior.
🪄 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: df96f5c6-fcde-4db5-88cb-8eb58234f0e6

📥 Commits

Reviewing files that changed from the base of the PR and between 89480e1 and 4773b64.

📒 Files selected for processing (7)
  • src/renderer/src/lib/terminal-reconciler.ts
  • src/renderer/src/lib/terminal-runtime-registry.dom.test.ts
  • src/renderer/src/lib/terminal-runtime-registry.ts
  • tests/term-fidelity/README.md
  • tests/term-fidelity/harness.ts
  • tests/term-fidelity/oracle.ts
  • tests/term-fidelity/workloads.ts

Comment on lines +247 to +340
// pear#421: Claude's idle ESC[?6n loop must not permanently close the
// reconciler quiet gate. Lock the intentionally tiny safety boundary: exact,
// complete query-only output is neutral; anything mixed, partial, or merely
// similar remains activity so snapshot races are gated when in doubt.
describe('terminal-runtime-registry — reconciler-neutral PTY queries', () => {
it('accepts one or repeated complete DEC-private cursor-position queries', () => {
expect(registry.isReconcilerNeutralPtyOutput('\x1b[?6n')).toBe(true)
expect(registry.isReconcilerNeutralPtyOutput('\x1b[?6n\x1b[?6n\x1b[?6n')).toBe(true)
})

it('rejects mixed, partial, non-private, and empty output', () => {
expect(registry.isReconcilerNeutralPtyOutput('\x1b[?6npaint')).toBe(false)
expect(registry.isReconcilerNeutralPtyOutput('paint\x1b[?6n')).toBe(false)
expect(registry.isReconcilerNeutralPtyOutput('\x1b[?6')).toBe(false)
expect(registry.isReconcilerNeutralPtyOutput('n')).toBe(false)
expect(registry.isReconcilerNeutralPtyOutput('\x1b[6n')).toBe(false)
expect(registry.isReconcilerNeutralPtyOutput('')).toBe(false)
})

it('still delivers a neutral query byte-for-byte to xterm', async () => {
const runtime = registry.acquireTerminalRuntime({
projectId: 'p',
agentName: 'query-agent',
terminalMode: 'drive',
theme: 'dark',
getInputSrtt: () => null
})
const term = createdTerminals[0]
runtime.mount(makeLayoutContainer())
await flushAsync()
await flushAsync()

const before = term.__writes.length
ptyBuffer.appendPtyChunk(runtime.key, '\x1b[?6n')
ptyBuffer.flushPtyChunksNow(runtime.key)

expect(term.__writes).toHaveLength(before + 1)
expect(term.__writes[before]).toBe('\x1b[?6n')
registry.disposeTerminalRuntime(runtime.key)
})

it('keeps the reconciler quiet through queries but closes it for paint and mixed batches', async () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'))
Object.defineProperty(document, 'visibilityState', {
configurable: true,
value: 'visible'
})
const runtime = registry.acquireTerminalRuntime({
projectId: 'p',
agentName: 'activity-agent',
terminalMode: 'drive',
theme: 'dark',
getInputSrtt: () => null
})
Object.defineProperty(runtime.host, 'clientWidth', { configurable: true, value: 800 })
Object.defineProperty(runtime.host, 'clientHeight', { configurable: true, value: 600 })
runtime.mount(makeLayoutContainer())
await flushAsync()
await flushAsync()

const deps = reconcilerHarness.deps
expect(deps).not.toBeNull()
const baselineSerial = deps!.activitySerial()
expect(deps!.isQuiet()).toBe(true)

// One rAF drain containing repeated exact queries: bytes still flow, but
// visible state is unchanged, so the serial/quiet gate must stay open.
ptyBuffer.appendPtyChunk(runtime.key, '\x1b[?6n')
ptyBuffer.appendPtyChunk(runtime.key, '\x1b[?6n')
ptyBuffer.flushPtyChunksNow(runtime.key)
expect(deps!.activitySerial()).toBe(baselineSerial)
expect(deps!.isQuiet()).toBe(true)

ptyBuffer.appendPtyChunk(runtime.key, 'paint')
ptyBuffer.flushPtyChunksNow(runtime.key)
expect(deps!.activitySerial()).toBe(baselineSerial + 1)
expect(deps!.isQuiet()).toBe(false)

await vi.advanceTimersByTimeAsync(RECONCILE_QUIET_MS)
expect(deps!.isQuiet()).toBe(true)

// Query + paint in the same coalesced write is activity. This prevents a
// query prefix from laundering a real repaint through the race guard.
ptyBuffer.appendPtyChunk(runtime.key, '\x1b[?6n')
ptyBuffer.appendPtyChunk(runtime.key, 'fresh paint')
ptyBuffer.flushPtyChunksNow(runtime.key)
expect(deps!.activitySerial()).toBe(baselineSerial + 2)
expect(deps!.isQuiet()).toBe(false)

registry.disposeTerminalRuntime(runtime.key)
})
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add a replay/duplicate-delivery regression case for the neutral-query gating.

writeChunks (the function under test here) is also invoked from the buffer-replay/catch-up paths in terminal-runtime-registry.ts (writeChunks(postSnapshotChunks), writeChunks(getPtyChunksSinceTotal(...))), not just the live subscribePtyBuffer callback exercised by these tests. None of the new cases cover a query-only or mixed batch delivered via replay (e.g. after a re-attach) to confirm the serial/quiet gate stays consistent when the same bytes are replayed rather than streamed live.

As per path instructions, "When changing broker start, event streaming, PTY buffering, spawned personas, or integration notifications, add regression tests covering duplicate and replay cases, not only successful first-run behavior."

🤖 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/renderer/src/lib/terminal-runtime-registry.dom.test.ts` around lines 247
- 340, Add a regression test covering replay or duplicate delivery through the
terminal runtime catch-up path, such as re-attachment or post-snapshot replay,
rather than only the live subscribePtyBuffer path. Verify that replayed
query-only batches preserve the reconciler activity serial and quiet state,
while replayed mixed query-and-paint batches increment the serial and close the
quiet gate. Anchor the test to writeChunks and the existing runtime/reconciler
harness symbols.

Source: Path instructions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-agent-relay-review Disable agent-relay automated PR review/fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

claude/codex CLI: stale terminal content overlaps new output (redraw/viewport divergence)

1 participant