fix(desktop): typing no longer freezes while the renderer polls topology#23601
Problem
The desktop renderer read local-environment topology with ipcRenderer.sendSync on every 3 s platform poll (getLocalEnvironmentBootstraps, getLocalEnvironmentEnabled), and getLocalEnvironmentEnabled also ran on composer renders. Each call parks the renderer thread until the main process answers, so any main-process stall freezes typing, even in an empty chat. The browser UI on the same backend has no desktopBridge and stays smooth. Reproduced on the real Linux desktop build at origin/main: typing into a new empty chat while the main process runs a 1 s blocking task every 3 s, the renderer froze inside sendSync for up to 885 ms (long tasks 885/554/476/464/350 ms), the same signature as the reporters' Windows profiles.
Change
- Main registers the existing bootstrap snapshot handler a second time as an async
invokehandler on the same channel. The sync handler stays for the one startup read. - The preload caches the bootstrap snapshot from that startup read and exposes
refreshLocalEnvironmentBootstraps, which refreshes the cache overinvoke.getLocalEnvironmentBootstrapsnow returns the cache. getLocalEnvironmentEnabledis read once per page. Changing it already relaunches the app.- The platform poll awaits the async refresh before it reads the primary target and the secondaries, so a topology change still lands within one poll. When the bridge has no refresh method, the web reader falls back to the sync read.
This re-lands the approach of https://git.cafe/pingdotgg/t3code/pulls/19300, which https://git.cafe/pingdotgg/t3code/pulls/22222 reverted only because it had not been run on a real desktop build. The refresh now runs before the primary read.
Scope and approval
Fixes #12689
GitCafe: https://git.cafe/pingdotgg/t3code/issues/12689
Triaged bug via the automated issue flow (labelled accepted, bug). The maintainer triage comment on the issue names this fix: stop polling the topology snapshot over sendSync.
Verification
Focused tests:
apps/desktop:vp test run src/preload.test.ts src/ipcpassed 20/20.polling topology reads do not block the renderer on synchronous IPC each timefailed on main withexpected 5 to be less than or equal to 1for both channels, and passes now.apps/web:vp test run src/connectionpassed 30/30, including the new async refresh case (secondary appears, failed refresh keeps the snapshot, empty refresh clears it).
Real desktop build, Linux (Electron 44.9.3 on Xvfb, --no-sandbox --disable-gpu), with existing user state seeded by vp run migrate-dev-db. A CDP probe typed 300 keys at 40 ms into a new empty chat while CPU-profiling the renderer. "busy" means the main process ran a 1 s blocking task every 3 s.
| run | sendSync sampled | longest sendSync | renderer long tasks > 200 ms |
|---|---|---|---|
| main, idle | 69 ms | 13.9 ms | 228, 216 |
| main, busy | 888 ms | 428 ms | 885, 554, 476, 464, 350 |
| fix, idle | 0 | 0 | none (max 176) |
| fix, busy | 0 | 0 | none (max 81) |
desktop.trace.ndjson: main made 693 sync topology calls in an 810 s session. The fix makes 2 sync calls at startup, then an asyncinvokeon the same channel every ~3 s. The sync and async handlers on one channel coexist in the real build.- Keystroke dispatch p99 under the busy main stays ~1.06 s both before and after, because Electron routes input through the main thread itself. The fix removes the renderer-side freeze, not main-process stalls.
- Full transcript: probe-transcript.md
The fixed build with existing user state connects to the local environment and loads the seeded threads. There is no visual change to compare, so these are after-only:
| New chat with seeded threads (fix) | Settings › Connections (fix) |
|---|---|
| after-newchat.png | after-connections.png |
Unverified here:
- Windows, where the issue was reported.
- macOS desktop.
- A WSL secondary backend appearing or disappearing mid-session.
Those paths run the same preload and web code. They are covered only by the unit tests above.
Claude Opus 5.5 (claude-opus-5-5) via Claude Code in T3 Code; reviews by Claude Opus 5.5 and Claude Sonnet 5.5.
🤖 Generated with Claude Code
Independent review and proof audit (head fc736575)
Reviews (read-only, full diff against main, with #19300 and #22222):
- Claude Sonnet 5.5:
VERDICT: MERGE. Found no concrete defect; the new tests pass. A topology change reaches both the primary and the secondaries within one poll, now that the refresh runs before the primary read. The sync and async handlers on one channel don't conflict. The fallback for older desktop shells is the old sync read. - Claude Opus 5.5:
VERDICT: NEEDS_JULIUS. Found no blocking defect. Its reason is the re-land bar from https://git.cafe/pingdotgg/t3code/pulls/19300 (a real build with user state on every platform it touches): only Linux has been run. Between polls, readers outside the poll see data up to one poll old. The bootstrap token can't go stale in that time because the backend accepts a 3-window, 12 h range. A newer desktop paired with an older web build would never refresh, but the desktop always loads its own bundled web.
Proof audit: pass 1 INSUFFICIENT 1,2,3,5. The runs weren't pinned to a SHA, the topology change had never been run on a real build, and the driver wasn't published. After new captures, pass 2 VERDICT: SUFFICIENT.
New rows. Real Linux desktop build (Electron 44.9.3, Xvfb, --no-sandbox --disable-gpu) with state seeded by vp run migrate-dev-db into an isolated home. before = 8d3b061121 (2 commits behind main, with no topology changes between them); after = fc736575.
| Row | before 8d3b061121 | after fc736575 |
|---|---|---|
| Startup, topology IPC in the first 10 s | 10 sync | 1 sync, then async invoke every ~3 s |
| Existing user state | threads load, connected (screenshot) | threads load, connected (screenshot) |
| Secondary added, removed, added, removed in main; when the renderer sees it | 2269 / 2496 / 677 / 2485 ms | 2535 / 2501 / 692 / 2489 ms (within one poll). A primary label change took ≤ 2490 ms. |
| Async refresh rejects for 5 polls (13 s) | N/A (no refresh method) | Snapshot kept the primary and the secondary throughout, with no offline banner. The first good poll applied a change 2472 ms after the handler was restored. |
Idle typing: sendSync ms / longest / long tasks > 200 ms / key p99 | 11.2 / 2.9 / none / 32 ms | 0 / 0 / none / 39 ms |
| Busy typing, random phase (5 before runs, 3 after) | 1 of 5 runs froze: 332.6 / 314 / [317] / 993 ms | 0 / 0. One run had a 569 ms long task with no sendSync (GC and editor work). |
| Busy typing, 1 s block aligned to the poll (2 runs each) | 1718.7 / 1009.9 / [1005, 702]; 1708.1 / 999.9 / [1000, 702] | 0 / 0 / none (max 67) |
The busy rerun confirms the body's table in mechanism: the same two preload sendSync frames, on before only. The size depends on the phase. A random-phase run hits a poll only sometimes; when the block is aligned to the poll, the before build froze for about 1 s in every run.
Driver ev-23601-driver.tgz, sha256 ce5a568d5250d512745f23e1585cc48147d307268bc0bc270e10b094e3e1253a (driver.mjs alone: f4b30c952194eb2c0b85f82f71db725ac5ae8fe75410b07294861bedd14a9eea). Results: captures.md, raw JSON and CPU profiles: ev-23601-data.tgz.
Verification log
- 2026-10-22, fs-17, head
fc736575710b11a7aa4c729aabee38d5eae3c907. Evidence only: no code changes and no pushes. - Reviews: Claude Sonnet 5.5 MERGE, Claude Opus 5.5 NEEDS_JULIUS.
- Proof audit: pass 1 INSUFFICIENT 1,2,3,5; pass 2 SUFFICIENT.
- Captures used the real Linux desktop build (
vp run build:desktopsucceeded; no libsecret workaround needed). - How the rows were injected: the secondary was injected into the main backend pool's live instance map through the main inspector, and the failed refresh replaced the async
invokehandler with one that throws. - Caveats:
- The first R1 attempt cloned the primary's URL and auth, which caused an offline banner. That run was discarded; the reported runs are from clean sessions.
- The before data file for busy random-phase run 1 was transcribed from console output after its profile was overwritten.
- The published driver is a later revision than the one that ran the before idle and busy runs.
- Audit pass 2 noted that two sentences in the body above don't match the transcript. The "885 ms inside
sendSync" was the longest long task; the longestsendSyncwas 428 ms. The keystroke p99 under busy main was 1167 ms before and 1064 ms after, not ~1.06 s both ways. Left for the author to correct. - Unverified: Windows (where https://git.cafe/pingdotgg/t3code/issues/12689 was reported), macOS desktop, and a real WSL secondary appearing or disappearing mid-session.
- Model and harness: Claude Opus 5.5 (claude-opus-5-5) via Claude Code in T3 Code (lead, capture, audits); reviewers Claude Sonnet 5.5 and Claude Opus 5.5.
Checks
- SuccessfulCodeRabbitGitHub
Review completed
- SuccessfulNative fingerprint diffGitHub
- Skipped
- Skipped
- Skipped
- SkippedRemove preview downloadGitHub
- SkippedBuild preview JS bundleGitHub
- SkippedDeploy web previewGitHub
- SkippedEAS PreviewGitHub
- SuccessfulNative fingerprint diffGitHub
- Skipped
- SkippedRemove preview downloadGitHub
- Skipped
- Skipped
- SkippedBuild preview JS bundleGitHub
- SkippedDeploy web previewGitHub
- SkippedEAS PreviewGitHub
- SkippedBuild preview JS bundleGitHub
- SkippedDeploy web previewGitHub
- SkippedEAS PreviewGitHub
- SkippedDeploy web previewGitHub
- SkippedBuild preview JS bundleGitHub
- SkippedEAS PreviewGitHub
- SuccessfulCheckGitHub
- SkippedRustGitHub
- Skipped
- SuccessfulTest WebGitHub
- SuccessfulBuild and TypecheckGitHub
- SuccessfulTestGitHub
- SuccessfulRelease SmokeGitHub
- SkippedEAS PreviewGitHub
- SuccessfulChangesGitHub
- SuccessfulLabel PR 23601GitHub
- Skipped
- SuccessfulTest Server 3GitHub
- SuccessfulLabel PR sizeGitHub
- Skipped
- Successful
- SuccessfulNative fingerprint diffGitHub
- Successful
- SuccessfulCollect PR targetsGitHub
- SuccessfulPrepare PR size configGitHub
- SuccessfulTest Server 5GitHub
- SuccessfulTest Server 4GitHub
- SuccessfulTest Server 1GitHub
- SuccessfulTest Server 2GitHub
- SuccessfulTest Server 7GitHub
- SuccessfulTest Server 8GitHub
- SuccessfulTest Server 6GitHub
- SuccessfulLintGitHub
Added 4 commits
Approvability
Verdict: Approved at fc73657
Macroscope's review found this PR approvable — This focused fix moves recurring desktop topology polling from blocking synchronous IPC to an asynchronous cached refresh while preserving existing topology and failure behavior. The added coverage validates cache reuse, refresh updates, and failure retention.
You can add or adjust custom eligibility rules. Learn more.
Thread transfer impact
✅ Thread transfer remains within every enforced ceiling.
| Provider | Metric | Main baseline | This PR | Impact | PR ceiling | |
|---|---|---|---|---|---|---|
| Codex | Total thread wire | 5.0 KiB | 5.0 KiB | +7 B (+0.1%) | 6.8 KiB | ✅ |
| Codex | Thread snapshot wire | 3.8 KiB | 3.8 KiB | 0 B (0.0%) | 4.9 KiB | ✅ |
| Codex | Live turn WebSocket wire | 1.2 KiB | 1.2 KiB | +7 B (+0.6%) | 2.0 KiB | ✅ |
| Codex | Live turn WebSocket decoded | 20.9 KiB | 20.9 KiB | 0 B (0.0%) | 29.3 KiB | ✅ |
| Codex | Live turn messages | 2 | 2 | 0 (0.0%) | 8 | ✅ |
| Claude | Total thread wire | 5.0 KiB | 5.0 KiB | 0 B (0.0%) | 6.8 KiB | ✅ |
| Claude | Thread snapshot wire | 3.8 KiB | 3.8 KiB | 0 B (0.0%) | 4.9 KiB | ✅ |
| Claude | Live turn WebSocket wire | 1.2 KiB | 1.2 KiB | 0 B (0.0%) | 2.0 KiB | ✅ |
| Claude | Live turn WebSocket decoded | 21.2 KiB | 21.2 KiB | 0 B (0.0%) | 29.3 KiB | ✅ |
| Claude | Live turn messages | 1 | 1 | 0 (0.0%) | 8 | ✅ |
Baseline: ed3f965 · PR result: fc73657 · Source CI: success
Scenario and decoded snapshot size
10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
- Codex decoded thread snapshot: 108.5 KiB
- Claude decoded thread snapshot: 108.8 KiB
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.
No actionable comments were generated in the recent review. 🎉
ℹ️ Recent review info
⚙️ Run configuration
- Source mode: Auto
- Repository: pingdotgg/t3code
- Branch: fix/issue-12689
- File: .coderabbit.config.ts
- Commit: fc73657
- Layers: Repository TS config
- Review profile: CHILL
- Plan: Team
- Run ID:
d70aabe0-e453-4a80-8f1b-59c7d532cf55
📥 Commits
Reviewing files that changed from the base of the PR and between ed3f965e0768f9a77729a17c5145a90ff3ab6210 and fc736575710b11a7aa4c729aabee38d5eae3c907.
📒 Files selected for processing: 8
apps/desktop/src/ipc/DesktopIpcHandlers.tsapps/desktop/src/ipc/methods/window.tsapps/desktop/src/preload.test.tsapps/desktop/src/preload.tsapps/web/src/connection/desktopLocal.test.tsapps/web/src/connection/desktopLocal.tsapps/web/src/connection/platform.tspackages/contracts/src/ipc.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.
📝 Walkthrough
The desktop bridge now caches synchronous bootstrap reads and supports asynchronous refresh. Web topology registration awaits that refresh before reading topology.
Changes: Desktop Bootstrap Refresh
Layer / File(s) Summary Asynchronous IPC and preload caching
packages/contracts/src/ipc.ts,apps/desktop/src/ipc/methods/window.ts,apps/desktop/src/ipc/DesktopIpcHandlers.ts,apps/desktop/src/preload.ts,apps/desktop/src/preload.test.tsThe desktop bridge adds an asynchronous bootstrap refresh method. The IPC handler shares bootstrap snapshot logic with the synchronous handler. The preload caches synchronous bootstrap and enabled-state reads, and replaces cached bootstrap data after refresh. Secondary topology refresh during registration
apps/web/src/connection/desktopLocal.ts,apps/web/src/connection/desktopLocal.test.ts,apps/web/src/connection/platform.tsThe secondary bootstrap reader adds asynchronous refresh and preserves its snapshot when refresh fails. Registration awaits refresh before reading topology. Priority: ➖ Normal
Estimated code review effort: 3 (Moderate) | ~20 minutes
Change: Bug fix · Severity of issue fixed: Medium
Sequence Diagram(s)
mermaid1 sequenceDiagram 2 participant RegistrationBuilder 3 participant DesktopSecondaryBootstrapsReader 4 participant DesktopPreload 5 participant DesktopIpcHandlers 6 RegistrationBuilder->>DesktopSecondaryBootstrapsReader: await refresh 7 DesktopSecondaryBootstrapsReader->>DesktopPreload: refreshLocalEnvironmentBootstraps 8 DesktopPreload->>DesktopIpcHandlers: asynchronous bootstrap IPC 9 DesktopIpcHandlers-->>DesktopPreload: bootstrap snapshot 10 DesktopPreload-->>DesktopSecondaryBootstrapsReader: refreshed bootstrap list 11 DesktopSecondaryBootstrapsReader-->>RegistrationBuilder: secondary bootstrap snapshot 12 RegistrationBuilder->>RegistrationBuilder: read primary topology Suggested reviewers:
maria-rcks,daniyal-sh
Merge Risk: ⚪ Minimal · up to fc736
The topology change appears mergeable after normal checks; the investigated cache behavior does not establish a material user-facing regression.
Pre-merge checks: Passed 4
Check name Status Explanation Title check Passed The title clearly and concisely describes the primary change: preventing desktop typing freezes during renderer topology polling. Linked Issues check Passed Issue #12689requires responsive desktop typing. The change removes repeated topology reads through synchronous IPC.preload.tscaches the startup snapshot and refreshes it through asyncinvoke. I…Out of Scope Changes check Passed The changes stay within issue #12689. Desktop IPC registration, preload caching, bridge typing, topology-reader refresh logic, platform polling, and focused tests all support removal of renderer-block…Description check Passed The pull request description includes all required sections: Problem, Change, Scope and approval, and Verification. It explains the reproduction, implementation, linked issue, focused test results, re… Full details: Linked Issues check
Explanation
Issue
#12689requires responsive desktop typing. The change removes repeated topology reads through synchronous IPC.preload.tscaches the startup snapshot and refreshes it through asyncinvoke. It also caches the enabled state.platform.tsawaits the refresh before it reads connection targets. The desktop and web tests cover single synchronous reads, async refresh, failed refresh retention, and empty refresh clearing. The reported Linux desktop test supports the renderer-freeze fix. Windows behavior remains unverified, but the issue does not require a platform-specific implementation or manual verification.
Full details: Out of Scope Changes check
Explanation
The changes stay within issue
#12689. Desktop IPC registration, preload caching, bridge typing, topology-reader refresh logic, platform polling, and focused tests all support removal of renderer-blocking topology polling. No unrelated product behavior or unrelated files are introduced.
Full details: Description check
Explanation
The pull request description includes all required sections: Problem, Change, Scope and approval, and Verification. It explains the reproduction, implementation, linked issue, focused test results, real desktop validation, screenshots, unverified platforms, and agent details. The description also discloses known measurement discrepancies and verification limits, but should correct those discrepancies before merge for accuracy.
✨ Finishing Touches
📝 Generate docstrings
- Commit to this branch
- Create a new PR
🧪 Generate unit tests (beta)
- Commit to this branch
- Create a new PR
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Comment @coderabbitai help to get the list of available commands.