Skip to content

feat(terminal): dump confirmed divergence corpus#400

Merged
khaliqgant merged 1 commit into
mainfrom
feat/reconciler-corpus-dump
Jul 17, 2026
Merged

feat(terminal): dump confirmed divergence corpus#400
khaliqgant merged 1 commit into
mainfrom
feat/reconciler-corpus-dump

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Jul 17, 2026

Copy link
Copy Markdown
Member

Summary

  • retain the guarded renderer viewport and broker plain snapshot at confirmed terminal divergence, then queue corpus collection only after the repair decision/write
  • write renderer.txt, broker.txt, screen.png, and meta.json under userData/term-fidelity-corpus/
  • coalesce concurrent duplicate triggers and enforce one capture per project+agent per five minutes
  • include dimensions, agent name, CLI, Relay dependency/broker versions, and reconciler telemetry lines
  • emit exactly one main-process success line per completed bundle; degrade failures to a per-agent rate-limited warning without throwing into reconciliation
  • defensively validate the renderer IPC payload before main-process property access

Verification

  • npx vitest run src/main/term-fidelity-corpus.test.ts src/main/ipc-handlers.test.ts src/renderer/src/lib/terminal-reconciler.test.ts src/renderer/src/lib/terminal-runtime-registry.dom.test.ts src/renderer/src/hooks/use-terminal.dom.test.ts (62 passed)
  • npm run typecheck
  • npm run build
  • targeted ESLint on all changed files

Coordination

The confirmed-divergence hook is deliberately small: one retained snapshot assignment at the confirm-twice point and one post-check queue in the outer finally block. This is intended to compose with the detection telemetry work on fix/codex-permission-divergence without changing its report ordering or repair gates.

No live broker instance or port 3889 was touched.

@khaliqgant khaliqgant added the no-agent-relay-review Disable agent-relay automated PR review/fixes label Jul 17, 2026
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 576973ae-a5fa-4c05-bf5a-d22ebacb1c52

📥 Commits

Reviewing files that changed from the base of the PR and between 2842951 and f5cb6ea.

📒 Files selected for processing (13)
  • src/main/ipc-handlers.test.ts
  • src/main/ipc-handlers.ts
  • src/main/term-fidelity-corpus.test.ts
  • src/main/term-fidelity-corpus.ts
  • src/preload/index.ts
  • src/renderer/src/components/terminal/TerminalInstance.tsx
  • src/renderer/src/components/terminal/TerminalPane.tsx
  • src/renderer/src/hooks/use-terminal.ts
  • src/renderer/src/lib/ipc-mock.ts
  • src/renderer/src/lib/terminal-reconciler.test.ts
  • src/renderer/src/lib/terminal-reconciler.ts
  • src/renderer/src/lib/terminal-runtime-registry.ts
  • src/shared/types/ipc.ts

📝 Walkthrough

Walkthrough

Adds a typed term-fidelity corpus IPC operation, main-process bundle writing with throttling and failure handling, and renderer hooks that capture confirmed terminal divergences with CLI and relay metadata.

Changes

Term fidelity corpus flow

Layer / File(s) Summary
Corpus IPC contract and preload bridge
src/shared/types/ipc.ts, src/preload/index.ts, src/renderer/src/lib/ipc-mock.ts
Defines corpus input/result types and exposes pear.app.dumpTermFidelityCorpus.
Corpus bundle writer
src/main/term-fidelity-corpus.ts, src/main/term-fidelity-corpus.test.ts
Validates inputs, coalesces concurrent captures, applies rate limits, writes bundle artifacts, and tests success and failure paths.
Main-process IPC integration
src/main/ipc-handlers.ts, src/main/ipc-handlers.test.ts
Registers the dump channel, captures the sender page, and supplies relay and broker version metadata.
Confirmed divergence capture
src/renderer/src/lib/terminal-reconciler.ts, src/renderer/src/lib/terminal-runtime-registry.ts, src/renderer/src/components/terminal/*, src/renderer/src/hooks/use-terminal.ts, src/renderer/src/lib/terminal-reconciler.test.ts
Reports confirmed mismatches and submits renderer, broker, and telemetry data while threading CLI metadata through terminal runtime acquisition.

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

Sequence Diagram(s)

sequenceDiagram
  participant TerminalReconciler
  participant TerminalRuntimeRegistry
  participant PearAPI
  participant IpcHandlers
  participant TermFidelityCorpusDumper
  TerminalReconciler->>TerminalRuntimeRegistry: confirmed divergence snapshot
  TerminalRuntimeRegistry->>PearAPI: dumpTermFidelityCorpus(input)
  PearAPI->>IpcHandlers: term-fidelity:dump-corpus
  IpcHandlers->>TermFidelityCorpusDumper: dump(input, capturePage, relayVersions)
  TermFidelityCorpusDumper-->>IpcHandlers: dumped path or failure reason
Loading

Possibly related PRs

Suggested reviewers: willwashburn, miyaontherelay

Poem

I’m a rabbit with snapshots tucked under my ear,
Capturing mismatches when divergence draws near.
Through IPC I hop, with telemetry bright,
Bundling screens in a timestamped night.
Throttle the carrots, let failures be small—
A tidy corpus trail for debugging all.

✨ 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 feat/reconciler-corpus-dump

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


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.

@khaliqgant
khaliqgant force-pushed the feat/reconciler-corpus-dump branch from 9abc7f0 to a8e654b Compare July 17, 2026 14:25

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a terminal-fidelity corpus dumper (TermFidelityCorpusDumper) to capture and dump terminal states (renderer text, broker text, screenshots, and metadata) when terminal divergence is confirmed. This functionality is wired up via Electron IPC handlers and integrated into the terminal reconciler on the renderer side, complete with comprehensive tests. The review feedback suggests improving robustness by defensively validating the untrusted IPC payload in validateInput to prevent potential main-process crashes, and using optional chaining when accessing dependencies from the parsed package.json to handle cases where it might parse to null.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/main/term-fidelity-corpus.ts Outdated
Comment on lines +28 to +50
function validateInput(input: TermFidelityCorpusInput): void {
if (!input.agentName.trim()) throw new Error('Term-fidelity corpus agent name is required')
if (!input.cli.trim()) throw new Error('Term-fidelity corpus CLI is required')

for (const [label, grid] of [
['renderer', input.renderer],
['broker', input.broker]
] as const) {
if (!Number.isInteger(grid.rows) || grid.rows <= 0) {
throw new Error(`Term-fidelity corpus ${label} rows must be a positive integer`)
}
if (!Number.isInteger(grid.cols) || grid.cols <= 0) {
throw new Error(`Term-fidelity corpus ${label} cols must be a positive integer`)
}
if (typeof grid.text !== 'string') {
throw new Error(`Term-fidelity corpus ${label} text must be a string`)
}
}

if (!Array.isArray(input.telemetryLines) || input.telemetryLines.some((line) => typeof line !== 'string')) {
throw new Error('Term-fidelity corpus telemetry lines must be strings')
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The validateInput function processes the input payload received from the renderer process via IPC. Since IPC payloads are untrusted and can be malformed or incomplete, we should defensively validate that input is a non-null object, and that its nested properties (agentName, cli, renderer, broker) exist and have the expected types before accessing their properties or calling methods like .trim(). This prevents potential unhandled TypeError crashes in the main process.

function validateInput(input: TermFidelityCorpusInput): void {
  if (!input || typeof input !== 'object') {
    throw new Error('Term-fidelity corpus input must be an object')
  }
  if (typeof input.agentName !== 'string' || !input.agentName.trim()) {
    throw new Error('Term-fidelity corpus agent name is required and must be a string')
  }
  if (typeof input.cli !== 'string' || !input.cli.trim()) {
    throw new Error('Term-fidelity corpus CLI is required and must be a string')
  }

  for (const [label, grid] of [
    ['renderer', input.renderer],
    ['broker', input.broker]
  ] as const) {
    if (!grid || typeof grid !== 'object') {
      throw new Error('Term-fidelity corpus ' + label + ' must be an object')
    }
    if (!Number.isInteger(grid.rows) || grid.rows <= 0) {
      throw new Error('Term-fidelity corpus ' + label + ' rows must be a positive integer')
    }
    if (!Number.isInteger(grid.cols) || grid.cols <= 0) {
      throw new Error('Term-fidelity corpus ' + label + ' cols must be a positive integer')
    }
    if (typeof grid.text !== 'string') {
      throw new Error('Term-fidelity corpus ' + label + ' text must be a string')
    }
  }

  if (!Array.isArray(input.telemetryLines) || input.telemetryLines.some((line) => typeof line !== 'string')) {
    throw new Error('Term-fidelity corpus telemetry lines must be strings')
  }
}

Comment thread src/main/ipc-handlers.ts
Comment on lines +55 to +64
const manifest = JSON.parse(
await readFile(join(app.getAppPath(), 'package.json'), 'utf8')
) as { dependencies?: Record<string, unknown> }
declaredRelayVersions = Object.fromEntries(
Object.entries(manifest.dependencies || {}).filter(
([name, version]) =>
(name === 'agent-relay' || name.startsWith('@agent-relay/')) &&
typeof version === 'string'
)
) as Record<string, string>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

If package.json parses to null (which is technically valid JSON), manifest will be null. Accessing manifest.dependencies would then throw a TypeError. Although this is wrapped in a try/catch block, using optional chaining (manifest?.dependencies) and typing manifest as nullable is more robust and prevents throwing unnecessary exceptions.

    const manifest = JSON.parse(
      await readFile(join(app.getAppPath(), 'package.json'), 'utf8')
    ) as { dependencies?: Record<string, unknown> } | null
    declaredRelayVersions = Object.fromEntries(
      Object.entries(manifest?.dependencies || {}).filter(
        ([name, version]) =>
          (name === 'agent-relay' || name.startsWith('@agent-relay/')) &&
          typeof version === 'string'
      )
    ) as Record<string, string>

@khaliqgant
khaliqgant force-pushed the feat/reconciler-corpus-dump branch from a8e654b to f5cb6ea Compare July 17, 2026 14:30
@khaliqgant

Copy link
Copy Markdown
Member Author

Coordinator merge note: the manual E2E gate (synthetic divergence → bundle on disk) is WAIVED with rationale — the term-fidelity matrix exercises this path organically: any reconciler repair event during a matrix run on a build containing this PR triggers the real capturePage+disk path (an opencode typing-during-stream repair occurred in today's validation run). First organic bundle will be verified there. All other gates met: hook design verified in review, ordering-lock + rate-limit + malformed-payload tests, CI fully green, single clean commit.

@khaliqgant
khaliqgant marked this pull request as ready for review July 17, 2026 14:46
@khaliqgant
khaliqgant merged commit 8cda152 into main Jul 17, 2026
5 checks passed
@cursor

cursor Bot commented Jul 17, 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 deleted the feat/reconciler-corpus-dump branch July 17, 2026 14:46

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

ℹ️ 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 +161 to +166
await mkdir(bundleDir, { recursive: true })
await Promise.all([
writeFile(join(bundleDir, 'renderer.txt'), input.renderer.text),
writeFile(join(bundleDir, 'broker.txt'), input.broker.text),
writeFile(join(bundleDir, 'screen.png'), screenshot),
writeFile(join(bundleDir, 'meta.json'), `${JSON.stringify(meta, null, 2)}\n`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bound retained corpus bundles

When terminal divergence persists over days or affects many agents, this automatic path creates a new full-page PNG and three additional files every five minutes per project-agent, but nothing prunes old timestamp directories or enforces a global size/count limit. The userData/term-fidelity-corpus directory can therefore grow without bound and eventually exhaust user disk space; apply an age, count, or byte-size retention policy when adding bundles.

Useful? React with 👍 / 👎.

void pear.app.dumpTermFidelityCorpus({
projectId: opts.projectId,
agentName: opts.agentName,
cli: opts.cli?.trim() || 'unknown',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Refresh CLI metadata on runtime reacquisition

When an agent is initially discovered with cli: 'unknown' and a later broker synchronization supplies its real CLI, the hook reacquires the existing runtime, but acquireTerminalRuntime only updates its theme and terminal mode. This callback remains closed over the options from the first runtime creation, so subsequent corpus bundles are mislabeled as unknown or another stale CLI until that runtime is disposed; update the runtime's diagnostic CLI value when it is reacquired.

Useful? React with 👍 / 👎.

khaliqgant added a commit that referenced this pull request Jul 17, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
khaliqgant added a commit that referenced this pull request Jul 17, 2026
…ath IPC lock (#401)

* fix(terminal): report reconciler divergence detection, not just repair

The quiet-time reconciler is two things: the convergence backstop (repair)
and the app's only always-on divergence detector. Every telemetry line hung
off a completed repair, which conflated them.

A divergence could be confirmed against the broker's ground truth and then
go entirely unreported: repairs are rate-limited to one per 15s, the ansi
re-fetch can return null, and output can race the snapshot or the final
flush — each of those returned silently. The detector also can't speak until
~10s of quiet (1.5s quiet gate + up to a check interval to align + a second
confirming check), so any observer sampling a shorter window reads zero
telemetry off a fully diverged screen and concludes the pipeline is clean.

That is what happened to the codex permission-prompt-repaint fidelity
bundle: 401 differing cells at 42x139, quiet, dims matched, no predictions
— and an empty reconcilerTelemetryLines array, because the checkpoint
captured at 2.1s of quiet. The same session proves the module works: the
resize workload sat quiet for 90s and the reconciler detected and repaired
it down to 1 cell.

Report at confirmed divergence, before deciding whether to repair, with the
row count and the repair outcome. Both lines keep the documented
`[terminal] viewport diverged from broker screen` prefix, whose documented
meaning is "a creation vector exists upstream" — true at detection, not only
at repair.

The gates themselves are untouched: confirm-twice, the repair rate limit,
the dims match and the activity-serial rechecks each guard a real
re-corruption path. This only makes them audible.

RECONCILE_DIVERGENCE_LOG_GAP_MS must stay strictly below
RECONCILE_MIN_REPAIR_GAP_MS; the first draft used 15s for both, which
re-silenced the repair-rate-limited case. A test locks the ordering.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(broker): lock one-IPC-per-worker_stream-event on the PTY fast path

The PTY fast path is the highest-volume IPC in the app, and the terminal
fidelity matrix traced a codex renderer/broker screen divergence to
somewhere in this pipeline. Whether a chunk can be delivered twice — or
suppressed when it shouldn't be — had no coverage at all, so the question
could only be answered by inference. It should be answerable by running a
test.

Three invariants, all currently held:
- one worker_stream event produces exactly one broker:pty-chunk send
- starting a project whose broker is already running (the reconnect path,
  which re-attaches a client) does not leave a second live listener
  double-sending to the same window
- byte-identical consecutive chunks are BOTH delivered: TUIs legitimately
  emit identical repaint frames and repeated keystroke echoes, and
  suppressing on content alone drops real bytes (AGENTS.md "Duplicate Event
  Hardening")

Why a doubled chunk would matter, for whoever reads this next: a duplicated
non-idempotent append puts a row on the grid the PTY never emitted, and a
diff-painting TUI then frames every later repaint against the wrong row —
stale panels and a whole-screen row shift that no later output heals.

These tests pass on main. That is the point: they were written to answer
"does Pear double-send PTY chunks?", and they establish that it does not.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: keep corpus telemetryLines format stable across #400/#401 merge

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.

1 participant