Conversation
Generated-by: Codex Signed-off-by: Dante <duanjl.china@gmail.com>
hqhq1025
left a comment
There was a problem hiding this comment.
Technical GO on exact head 99135613c7c9ee00dcbd56ab3865bda4231fc707; I found no P0-P2 issues. One P3 test-coverage gap is noted inline.
The implementation fixes the actual ownership boundary: each broadcast captures the current WebFrameMain, checks that exact renderer generation, and sends through the same object. A real Electron 43.4.1/Xvfb stress probe completed 14,100 broadcasts across forcefullyCrashRenderer() and reload without the prior Error sending from webFrameMain, while broadcasts resumed after a live replacement frame appeared.
Review conclusions:
- The solution is optimal for the reported problem and stays at the shared broadcast boundary.
- Production code to delete: none identified.
- Tests to delete: none; the existing broadcast test should be strengthened as described inline.
- No deeper refactor is required.
- The revision is technically merge-ready; the P3 is non-blocking, and the final decision remains with a maintainer.
- Residual gaps: the exact crash/reload behavior was exercised on Linux/Xvfb, not native Windows or macOS.
This changes renderer-lifecycle IPC delivery behavior, so independent human review is required.
Verification passed build:test, full typecheck/lint/format, ASF headers, changed-file Biome, git diff --check, focused Desktop lifecycle tests 36/36, the real Electron crash/reload probe, all hosted checks, and a clean merge with current main bf6e9422960401da6bba01d03a8ea9979722bb5a. The full Desktop run was 2470 passed / 8 cancelled; those cancellations are in unchanged current-main MCP OAuth deadline tests and are not attributable to this two-file change.
Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.
| } as unknown as Electron.WebFrameMain; | ||
| const renderer = Object.assign(new EventEmitter(), { | ||
| isDestroyed: () => false, | ||
| mainFrame, |
There was a problem hiding this comment.
[P3] Exercise the unavailable-frame branch instead of only adapting the mock
This test now supplies mainFrame.send, but both WebContents and the frame stay permanently live. It therefore does not verify the defect being fixed: a non-destroyed WebContents whose mainFrame is destroyed or throws during renderer replacement, followed by a live replacement frame. For example, removing the frame.isDestroyed() check while keeping mainFrame.send() would leave this test green. Please drive the same registered renderer through destroyed-frame, accessor-throw, and replacement-live states, asserting no delivery/no throw during the gap and delivery after recovery.
There was a problem hiding this comment.
Automated Codex follow-up: I added the requested regression coverage in 4982416. The existing auxiliary-renderer test now drives the same registered renderer through a destroyed main frame, a mainFrame accessor that throws during replacement, and a live replacement. It verifies that both unavailable phases neither throw nor deliver, then confirms delivery resumes after replacement.
Validation:
npm run buildnode --test --test-force-exit apps/desktop/dist/main/__tests__/workhub-presentation.test.js(29 passed)npm run lint,npm run format:check,npm run typechecknpx knip --workspace apps/desktop,npx knip --workspace packages/ui
I also temporarily bypassed the frame-destruction guard: the new no-delivery assertion failed as intended; restoring the guard returned the suite to 29/29.
Generated-by: Codex Signed-off-by: Dante <duanjl.china@gmail.com>
c266eef to
4982416
Compare
hqhq1025
left a comment
There was a problem hiding this comment.
Technical GO on exact head 49824162834d0dc8b4d0af2bd0b4d25c4c1bf83c; I found no remaining P0-P3 issues.
The new test closes the previous coverage gap. It now verifies all three relevant renderer-generation states at the shared broadcast boundary: a destroyed WebFrameMain is skipped, a transient webContents.mainFrame access failure is contained, and delivery resumes exactly once after a live replacement frame appears. The production implementation is byte-identical to the previously reviewed head, so the earlier Electron 43.4.1/Xvfb stress result still applies: 14,100 broadcasts across forcefullyCrashRenderer() and reload completed without Error sending from webFrameMain, and delivery resumed after recovery. The current exact head also passed the real WorkHub crash/recovery Electron journey.
Review conclusions:
- The solution is optimal for the reported lifecycle race and remains localized at the shared broadcast boundary.
- Production code to delete: none identified.
- Tests to delete or replace: none; the added regression directly exercises the formerly missing states.
- No deeper refactor is required.
- The revision is technically merge-ready; GitHub remains
REVIEW_REQUIRED, so the final decision belongs to an independent maintainer. - Residual gaps: the crash/recovery path was exercised on Linux/Xvfb, not native Windows or macOS.
This changes renderer-lifecycle IPC delivery behavior, so independent human review is required.
Verification passed build:test, full typecheck/lint/format, ASF headers, changed-file Biome, renderer architecture 112/112, E2E budget 38, focused WorkHub presentation 29/29, the real Electron WorkHub crash/recovery E2E 1/1, hosted test/package, git diff --check, and a clean merge with current main f32cf2b482a17f235b92e2775ed2672c2775e219. The full Desktop run was 2470 passed / 8 cancelled; the same eight cancellations reproduce in the unchanged MCP OAuth deadline suite when run alone and are not attributable to this two-file change.
Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.
Generated-by: Codex Signed-off-by: Dante <duanjl.china@gmail.com>
hqhq1025
left a comment
There was a problem hiding this comment.
Reviewed exact head 4f0ee7e5bb25526eaf770b08432447adbe43131f.
I found no P0-P3 issue in this two-file renderer-frame guard change. apps/desktop/src/main/main-window.ts:144-160 captures and validates each recipient's current WebFrameMain, contains the transient mainFrame access failure during renderer replacement, skips destroyed frames, and resumes delivery through the replacement generation. The merge resolution in apps/desktop/src/main/__tests__/workhub-presentation.test.ts:432-471 preserves both that three-state regression and current main's renderer-parent ownership assertions.
Exact-head validation passed: clean install with Node 24.18.1, build:test, full workspace typecheck, Desktop 2535/2535, focused WorkHub presentation 29/29, renderer architecture 112/112, E2E budget, production renderer build, lint, format, ASF headers, changed-file Biome, git diff --check, and the real WorkHub crash/recovery Electron E2E under Xvfb. A conflict-free synthetic merge onto current main cb4747171e742bb0fb8187d1f0206c1591aac982 also passed clean build, full typecheck, the focused 29/29 suite, architecture, and E2E budget checks. Hosted package succeeded.
Hosted test is red only in the unchanged transcript-scroll-cost E2E; that file and its production scroll authority are outside this PR, and the same case passed locally 2/2, so I did not attribute the failure to this change. The required gate still needs a successful rerun or separate resolution before merge.
I did not independently exercise native macOS crash recovery.
Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.
Generated-by: OpenAI Codex
hqhq1025
left a comment
There was a problem hiding this comment.
Reviewed exact head 91cb8da6409cd72772425dfed2c6c87949840010.
I found no P0-P3 issue. This head merges current main c87651e23d2069fdaed687c785a8f965e211c15e; relative to that main, the effective PR diff remains the two-file renderer-frame guard. apps/desktop/src/main/main-window.ts:143-163 captures and validates each recipient’s current WebFrameMain, contains the transient mainFrame access failure during renderer replacement, skips destroyed frames, and resumes delivery through the replacement generation. The merged test fixture in apps/desktop/src/main/__tests__/workhub-presentation.test.ts:451-503 retains the destroyed-frame, getter-throw, and replacement-frame regression while also adopting current main’s shared native View container and renderer-parent assertions.
Exact-head validation passed with Node 24.18.1: clean install, build:test, full workspace typecheck, Desktop 2538/2538, focused WorkHub presentation 31/31, renderer architecture 112/112, E2E budget 37, production renderer build, lint, format, ASF headers, git diff --check, and the real WorkHub renderer crash/recovery Electron journey under Xvfb. Hosted test and package are both successful, and GitHub reports the PR MERGEABLE.
I did not independently exercise native macOS crash recovery.
Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.
Astro-Han
left a comment
There was a problem hiding this comment.
Review
Reviewed the two-file diff against #5135. I agree with the diagnosis and the fix lands at the right seam. No blocking issues; the points below are nits.
What I verified
- Root cause and guard placement.
apps/desktop/src/main/main-window.ts:144-151is the single shared broadcast boundary for everymainWindowController.send(...)caller (runtime-host-boot, pet-pack-import, desktop-shell-presentation, browser-view state/live). #5135's repro showsWebContents.isDestroyed() === falsewhile the main frame is already disposed, so liveness now has to be answered by the frame — this is the correct authority. - Liveness check and delivery address the same generation.
liveMainFramecaptures theWebFrameMainonce andsafeSendToRenderersends through that same object, so the check cannot pass for one generation and deliver to another.contents.isDestroyed()is still checked first (main-window.ts:154), which is required since Electron throws on property access for a destroyedWebContents. - Delivery scope is unchanged in practice. #5135's stack shows
WebContents.sendalready delegating intoWebFrameMain.send, so narrowing tocontents.mainFramematches existing behavior. I also confirmednodeIntegrationInSubFramesis not enabled anywhere inapps/desktop, and the app's only subframe is the sandboxedsrcdocartifact iframe (apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-preview.tsx:215-218,sandbox="allow-scripts"→ no preload, nowindow.maka). Main-frame-only delivery also matches the repo's inbound convention (event.senderFrame !== event.sender.mainFrameatapp-ipc-main.ts:70,browser-ipc-main.ts:144,workhub-control.ts:327,workhub-presentation.ts:504). - IPC/security surface. Outbound-only change: no new channels, no new inbound handlers, no change to how
...argsare serialized. Nothing to validate here. - Regression test goes through the production authority.
workhub-presentation.test.ts:451-496esbuild-bundles the realmain-window.tsand runs it in a fresh context rather than reimplementing the loop, and it does fail without the change: the fakeWebContentsno longer definessend, so the oldcontents.send(...)throws and the firstassert.doesNotThrowat line 488 fails. All three states (destroyed frame, throwingmainFramegetter, resumed delivery on the replacement frame) are covered, and the "exactly once" assertion at line 496 is preserved. - Hosted
testandpackageare green on head91cb8da; no new dependencies; barecatch {matches the prevailing style inapps/desktop/src/main.
Nits
-
contents.isDestroyed()early return is now uncovered. The only fake registered in the test never reports destroyed, and oncerenderer.emit('destroyed')fires the release listener removes it from the map — so the broadcast loop never observes a destroyedWebContents. That branch (main-window.ts:154) carried over from the old code unchanged; one line (mark the fake destroyed while still registered, assert no throw and no send) would pin it. -
No multi-recipient isolation assertion. Only one renderer is registered, so "one recipient with a dead/throwing frame does not suppress delivery to the others" is not asserted even though the loop now has a skip-and-continue shape. A second registered fake with a live frame would cover it cheaply.
-
Sibling broadcast path keeps the old predicate.
apps/desktop/src/main/workhub-presentation.ts:100-103still guards with!wc.isDestroyed()alone, plus direct sends at:120,:247,:441. Those are event-driven rather than per-session-event, so the stderr-flood risk is much lower, but the exposure window is the same one described in #5135. If "shared broadcast boundary" is the intent, exportingliveMainFrame(or a smallsendToLiveMainFrame) and reusing it here would remove the duplicate predicate. Worth an explicit in/out-of-scope decision in the PR rather than a follow-up issue. -
Harness fake
mainFrame = {}is a latent trap.workhub-presentation.test.ts:66gives the fakeContentsamainFramewith noisDestroyed/send. Harmless today because the harness controller sends viawc.send, but any future test that routes a fake throughsafeSendToRendererwould land inliveMainFrame's catch and silently receive nothing — a confusing "no message" failure instead of a clearTypeError. Making the fake mirror production (isDestroyed: () => false,send: (c, ...a) => this.sent.push([c, ...a])) removes the trap. -
Optional diagnostic in the catch.
main-window.ts:158-162also swallows genuinely unexpected failures (e.g.frame.isDestroyed()throwing). The file already reports renderer lifecycle failures viaconsole.error('[renderer] …')(:237,:243,:629); a first-occurrence-only log would keep the flood fix from #5135 while preserving debuggability. Fully optional — silence is defensible and conventional. -
Checklist. "Tests cover the change and fail without it" is unchecked but is in fact satisfied (see above). Worth ticking.
Residual gap (not a blocker)
The guard is best-effort: a frame can be disposed between frame.isDestroyed() returning false and frame.send() executing. Per #5135, Electron logs those internally rather than throwing, so no JS-side try/catch can close that window — the guard narrows it to a very small race. Worth stating in the PR body so the fix isn't read as eliminating the class entirely.
Address Astro-Han review by covering destroyed WebContents and delivery to a healthy recipient during frame replacement. Give the harness frame the production send interface. Generated-by: OpenAI Codex
|
Automated Codex follow-up: I addressed the coverage nits in the existing regression: destroyed-but-still-registered WebContents is skipped, and a second live recipient continues receiving during both destroyed-frame and throwing-accessor gaps. The harness mainFrame now implements isDestroyed/send. Build, lint, format, typecheck, both knip checks, and the focused WorkHub/startup suites pass (38 tests). I also checked the regression checklist item and documented the best-effort race. I kept the sibling WorkHub event-driven sends outside this high-frequency broadcast fix; sharing the helper there would expand a separate delivery path. I left the catch silent to avoid restoring repeated diagnostics during expected renderer replacement. |
Summary
WebFrameMainWebContents.isDestroyed()is insufficient during renderer-process loss and navigation because theWebContentscan outlive its current main frame. Capturing the frame before checkingisDestroyed()avoids askingWebContents.send()to resolve a disposed generation internally.The frame guard is best-effort: Electron can still dispose a frame between the liveness check and
send(), and internally logged send failures are not intercepted by JavaScripttry/catch.Scope: this change protects the shared, high-frequency application broadcast boundary. WorkHub presentation’s separate event-driven sends remain outside this fix; extending the helper there would change a separate delivery path. The catch stays silent because expected replacement gaps must not reintroduce repeated renderer diagnostics.
Fixes #5135
Verification
npm run buildnode --test --test-force-exit apps/desktop/dist/main/__tests__/workhub-presentation.test.js apps/desktop/dist/main/__tests__/main-startup-lifetime.test.js(36 passed)npm run lintnpm run format:checknpm run typechecknpx knip --workspace apps/desktopnpx knip --workspace packages/uinpx biome check apps/desktop/src/main/main-window.ts apps/desktop/src/main/__tests__/workhub-presentation.test.tsgit diff --checkCI follow-up
The previous Linux E2E job reported a 4683px transcript range-boundary displacement. This branch now includes current main’s visible-Turn residual anchoring correction from #5374. The unchanged transcript-scroll scenario is included in the current-main run with 37 E2E tests passing.
After merging main, build, lint, format, typecheck, both knip checks, and the existing lifecycle plus scroll-authority suites pass (61 tests). The local target Electron E2E failed at process launch with SIGABRT before running the scenario; this is not a passing local E2E result.
Review follow-up
The existing broadcast regression now also covers a destroyed-but-registered
WebContentsand a healthy second recipient during destroyed-frame and accessor-throw gaps. The harness frame implementsisDestroyed()andsend(). After this test-only change, build, lint, format, typecheck, both knip checks, and the focused WorkHub/startup suites pass (38 tests).AI use
Select exactly one:
Tool(s) and scope: Codex traced the Renderer recovery path and implemented the frame-generation broadcast guard.
Checklist
Does this PR entail a change in behavior?