fix(pierre): recover failed highlight worker pools - #8300
Conversation
c9bcd49 to
c7c007a
Compare
UX Review (Fable 5, fork) — 🟡 CONCERNSUX-level review of UX-Verdict: CONCERNS Recovery keeps content readable, but the new fallback states shipped unseen, degrade silently, and the terminal state gives users no way to know a reload restores highlighting. Watch
Evidence gaps
[UX-REVIEWED] 5e7baf2 |
Design Review (Fable 5, fork) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS Sound generation-owned lifecycle, but it couples to Pierre's private worker wire protocol and lets one slow request spend the tab-wide failure budget. Watch
Suggestions
[DESIGN-REVIEWED] 5e7baf2 |
First Principles Review (Fable 5, fork) — 🟡 CONCERNSPremise-level review of All evidence is gathered: the fix has a linked issue (#8268), the deleted "one-way on purpose" pin's stated premise (no readiness signal to read from a live pool) is answered by the new design's First-Principles-Verdict: CONCERNS
Not justified as shipped
What this change shipsInventory (10 items) — 7 justifiedIntent: a FIX — one worker failure must stop permanently moving highlighting onto the UI thread and instead recover (#8268).
The deleted Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] 5e7baf2 |
Opus 4.8 Review (fork) — ✅ no blocking findingsReviewed Review detailsThe single candidate rests on the behavior of No findings. [OPUS-REVIEWED] 5e7baf2 |
GPT 5.6 Review (fork) — ✅ no blocking findingsReviewed Review detailsNo findings. |
c7c007a to
fd2e610
Compare
|
fd2e610 to
12ccb72
Compare
12ccb72 to
5db6879
Compare
|
|
cc6881d to
229605c
Compare
|
229605c to
976df96
Compare
976df96 to
9747711
Compare
9747711 to
90d6ad3
Compare
|
Rebased onto main Clean rebase, no conflicts, no content changes: the diff against main is still 12 files, +1112/-378, identical to the pre-rebase diff. Gates run locally in an isolated worktree:
Please sanity-check the rebased branch. Note that a maintainer push makes the maintainer the last pusher, so under this repo's last-push rule a second approver is needed. Reply here if anything looks wrong. |
90d6ad3 to
5e7baf2
Compare
bolichen97
left a comment
There was a problem hiding this comment.
Requesting changes at 5e7baf2 — this is shipped as a "focused standalone worker-lifecycle fix" under a <!-- no-visual-delta --> waiver, but it changes rendering on every code surface and carries an unrelated behavior change.
- Rendering changes on all surfaces.
PierreImpl.tsx(PierreCodeImpl/PatchImpl/FilePairImpl) now renderPlainCodeFallbackuntilphase === 'ready', so every code/patch/file-pair surface has a new cold-start plain-text phase until 4 workers + WASM initialize, where main mounted Pierre immediately.PierreEditorImpl.tsx~L266-275 turns the editor into a bare<textarea>(no gutter, line numbers, diff, markers) duringstarting/recovering/cooldown/unavailable.PlainCodeFallback.tsx~L27 adds a new 36 pxdata-diffs-headerrow to fallbacks, and PierrePatch now renders header metadata inside the fallback while suppressing it on the impl untilimplVisible. Those are rendered deltas; the waiver is inaccurate. Per the repo rule this is a UX change and needs a maintainer decision, not a bug-fix approval. - Smuggled behavior change.
website/src/utils/domHighlight.ts~L20-27 makes transcript search skip every[aria-hidden="true"],[inert],[hidden]subtree. That changes search occurrence counts app-wide, is absent from "What changed", and surfaces only under "Tests" as the fix for the previously failing Markdown search integration. Split it out or disclose and justify it. - WarmSwap regression vs the body's claim. The in-impl fallback's non-empty
<pre>satisfiesscrollHeight > WARM_PAINT_MIN_PX, sopaintedflips true,warmSwapHeightsrecords the FALLBACK height, and the later fallback→Pierre swap happens outside the height freeze — the opposite of "preserves #7916's warm-swap height stabilization". - Uncontrolled editor callers regress render inputs.
setDraftContentson every keystroke feeds<File edit>a newfile/cacheKeyper keystroke when the parent does not echo contents (EditableCodeBlock.tsx~L78-79 keeps onlyvalueRef); Pierre'suseFileInstancethen callsinstance.render({file})withnewContent=trueinside an active edit session. Main keptfilestable here; the tests only exercise mocked surfaces.
Smaller: publish-before-terminate ordering relies on React's sync-lane microtask beating queueMicrotask(() => handle?.terminate()) (workerPoolLifecycle.ts ~L116); FilePairPlainFallback duplicates the existing PlainFilePairFallback and drops its i18n label/split handling; pierre.workerHealth.test.tsx keeps its name after workerHealth.ts is deleted; in any Worker-less environment every surface is now permanently plain text where main used Pierre on the main thread.
Problem / Motivation
A Pierre highlight worker failure currently latches
disableWorkerPoolfor the rest of the browser tab. This moves later Shiki work onto the renderer main thread and still does not reliably recover already-mounted imperative Pierre renderers.Workers that remain alive but stop responding also have no request timeout, so their surfaces can wait forever.
Why it matters
Code-heavy sessions can become unresponsive after one worker failure. The fallback intended to prevent blank code blocks can instead move expensive highlighting onto the UI thread for every later code and diff surface until reload.
Mounted surfaces, collapsed file rows, unsaved editor drafts, editor diagnostics, and file-header interactions also need to remain intact while recovery is in progress.
What changed (motivation → approach → change)
Replaced the one-way worker-health latch with an application-owned, generation-based lifecycle:
disableWorkerPoolas recoveryerror,messageerror, and synchronouspostMessagefailure as pool failuresworkerHealth.tslatchDuring recovery the editor swaps to an app-owned plain textarea rather than invoking Pierre without workers. This keeps typing and save available without moving Shiki onto the renderer thread; the latest draft remains visible and remounts into Pierre when a replacement pool is ready.
Relationship to Pierre work
This remains a focused standalone worker-lifecycle fix:
Keeping worker recovery separate gives it an independent rollback boundary and avoids coupling a correctness/resilience fix to broader rendering redesigns.
Tests
Added or expanded deterministic coverage for:
error,messageerror, synchronous posting failure, and hung-request recoveryfile.contentsupdates superseding retained draftsshiki-wasmuse and absence ofdisableWorkerPoolValidation performed:
workerPoolLifecycle.tsfocused coverage: 85.39% statements and 84.48% branches (above the 80% per-file floor)Manual verification
No live gateway or runtime configuration was modified. Worker failures, stale events, hangs, retry/cooldown boundaries, mounted fallback, file-header interactions, draft retention, and editor remounting were exercised with deterministic worker and manager doubles.
Screenshots / video
Why no screenshot: This changes worker failure/recovery mechanics and reuses the existing plain-text fallback styling; it introduces no intentional layout, component, theme, or copy change.
Related Issues
Fixes #8268
Pattern harvest
Rule candidate: review-prompt
Pattern: A failed worker must degrade to app-owned output and replace its complete lifecycle owner; it must not recover by moving unbounded work onto the UI thread.
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)