Skip to content

Main-process exceptions no longer block quitting (D28): the check, the strip's fade timer, two test races - #11

Merged
siyabendoezdemir merged 7 commits into
mainfrom
lead/quit
Oct 10, 2026
Merged

siyabendoezdemir merged 7 commits into
mainfrom
lead/quit

Conversation

@siyabendoezdemir

@siyabendoezdemir siyabendoezdemir commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Was stacked on PR #10 (base lead/polish2), since both change tests/e2e/helpers.ts. #10 has merged, so the base is main now.

What

  • Product. If Orbit's main process hits an exception nothing catches, Electron's default shows a modal error box. The box holds the main thread until someone dismisses it, so Orbit can't quit. A user would see a stack trace in a box, then an app that won't quit. Now the exception is logged ([orbit] uncaught exception: …), and Orbit carries on (src/main/index.ts).
  • E2E harness (tests/e2e/helpers.ts). app.close() now checks the quit:
    • a test fails, showing the stack, if its main process logged such an exception, including one thrown while quitting. orbit.takeUncaught() claims one thrown on purpose;
    • if Orbit still hasn't quit 15 s after being asked, the test fails and Orbit is killed. One stuck quit then fails one test instead of hanging the whole run.
  • The exception behind the SV-03 hangs, found by the check and fixed (13a2494, src/main/split/strip.ts):
    • the split's pane strip fades out for 120 ms before its view hides. If its window closed during the fade, which happens when a test quits right after the strip hid, the fade's timer still ran;
    • by then view.webContents was undefined, so reading isDestroyed() threw TypeError: Cannot read properties of undefined (reading 'isDestroyed');
    • now the window's closed handler clears the strip's show, hide and fade timers, and every check of the strip's view handles a view whose page is gone.
  • Decision D28. docs/ARCHITECTURE.md §9 explains the check.
  • Two test races from the full run with the check (no product change):
    • MW-10 (5efc435): fromMini sends a command from the Mini Window's own page. A close can shut that page before Playwright gets the evaluate's reply ("Target page, context or browser has been closed"). The error is now ignored, but only when the page really closed.
    • SV-17 (721fdc8, cherry-picked from the chrome lane's 442766d): a pane added a moment ago grows in from zero width (SV-26), so it may have no box yet. The views-over-panes poll threw on that and failed at once; now it retries.

Why

  • Since 20:39 UTC, five runs ended with a test hanging in its afterEach's app.close(), then "Worker teardown timeout of 60000ms exceeded". That fails the job even when the test passes on retry:
  • Playwright's close evaluates app.quit() through the inspector, then waits for the process to exit. No Orbit handler cancels a quit.
  • Reproduced: a main-process setTimeout that throws makes app.close() hang, with the main thread parked in do_poll (the box's nested loop). A rejected promise doesn't.
  • With the check in place, 15 repeats each of SV-03 and TB-01 logged one exception, in SV-03: the strip's fade timer above. That explains the SV-03 hangs. The TB-01 and macOS hangs (CB-21, MW-14) haven't shown theirs yet; when they do, the check names the test and the stack instead of hanging.

Tests

  • New: [TB-22] an exception nothing caught in the main process is logged; Orbit still quits and restores (D28) (core.spec.ts). It throws in a main-process timer, checks the log, quits and relaunches with the same profile.
    • Without the fix it fails. Nothing is logged, and the quit hangs until the harness kills Orbit at 15 s: still running (main thread: do_poll.constprop.0).
    • With the fix it passes.
  • New: [SV-12] a window that closes while its pane strip fades out closes cleanly (D28) (split.spec.ts). It opens a second window so Orbit keeps running, starts the strip's fade, closes the strip's window, and waits past the fade.
    • Without the fix it fails 3/3 with the TypeError above. With the fix, it and the three other strip tests passed 20/20 (5 runs each).
  • Scratch checks, since deleted:
    • a quit that never happens (before-quit prevented) is killed at 15 s, and its test fails;
    • a clean quit passes and takes 100–350 ms. 40 launch, split and close cycles under load had no hang.
  • Full suite at 4fe445a (1440×900, 1 worker, 11.9 min): 216 passed, 2 failed. The failures were MW-10 and SV-17, the two test races above. No test logged an uncaught exception, and no quit needed the kill. After the fixes, MW-10 and SV-17 passed 12/12 (6 runs each).
  • With the strip fix, 20 repeats each of SV-03 and both TB-01 tests: 60/60 passed, with no exception logged.
  • Full suite at 13a2494 (1440×900, 1 worker, 9.4 min): 219/219 passed. No exception logged and no kill.
  • npm run typecheck and npm run lint exit 0; unit tests 252/252.

Review

  • Independent review (reviews/lead-quit.md in the Orca context, at 13a2494): APPROVE WITH FIXES, 1 must-fix and 6 follow-ups.
    • It reproduced the root cause with the real bug. On the base build, the strip's fade timer firing after its window closed hung 5 of 8 quits, each with the main thread in Electron's error box. With this PR, all 16 such quits took 111–390 ms.
    • Its full suite at 1440×900 passed 218 of 219; the one failure was CB-22's load-sensitive budget.
    • The TB-01 and macOS hangs (CB-21, MW-14) are still unexplained. 102 repeats of those tests and 76 quits with timers pending found no other exception.
  • Lead fix (D18), 504872a (test harness only): the quit check missed an exception whose log ran past about 4,000 characters, because it trimmed stderr before looking for the marker. It now looks first and keeps only a marker-length tail otherwise. A new TB-22 test throws a 5,000-character message: it fails without the fix (takeUncaught() returned '') and passes with it, 3/3. core + split + smoke e2e: 62/62.
  • 7eaec40 merges PR Lead polish 2: full-split drop target, Favorite splits past 12, e2e evaluate race #10's two review fixes into this branch; the reviewer checked them at d2e4b8d.
  • d19b9da, CI only (D18): the two Linux jobs are pinned to ubuntu-24.04. GitHub moves ubuntu-latest to Ubuntu 26 from 2026-10-19, and the e2e job's apt packages are only known to work on 24.04, the image ubuntu-latest runs today.
  • The follow-ups are tracked in the lead's STATUS.md:
    • start the 15 s clock when the quit reaches main;
    • check a quit that happens on its own;
    • log unhandled promise rejections the same way;
    • take a sample of the stuck process on macOS before the kill;
    • write the log to a file in packaged builds;
    • guard Peek's settle timer like the strip's.

Local check (merge gate, D42)

GitHub Actions is off for cost, so this local check is the merge gate. On d19b9da, in this PR's worktree (Linux, Xvfb, one worker):

npm run build; npm run typecheck; npm run lint; npx vitest run
xvfb-run -a -s "-screen 0 1440x900x24" ./node_modules/.bin/playwright test --workers 1 --retries 0

🤖 Generated with Claude Code


Note

Medium Risk
Changes main-process error handling and split strip lifecycle timers; behavior on real bugs shifts from a blocking dialog to continued execution, which is intentional but affects how failures surface in production.

Overview
Uncaught main-process exceptions no longer block quit: Electron’s modal is replaced by logging [orbit] uncaught exception: in src/main/index.ts (D28).

Split pane strip (strip.ts) clears show/hide/fade timers when the window closes and uses a live() guard so fade timers don’t touch destroyed views—the failure mode that hung app.close() in E2E.

E2E harness (helpers.ts): launchOrbit wraps app.close() to fail on any uncaught main exception (with takeUncaught() for intentional throws), kill the app after 15s if quit stalls, and detect the marker even when stderr writes exceed ~4k chars. New TB-22 / SV-12 tests cover logging, quit/restore, long logs, and strip fade + window close. MW-10 ignores evaluate errors when the Mini Window page closes first; SV-17 polling tolerates panes still growing in (SV-26).

CI pins Linux jobs to ubuntu-24.04 instead of ubuntu-latest; ARCHITECTURE.md §9 documents the harness behavior.

Reviewed by Cursor Bugbot for commit d19b9da. Bugbot is set up for automated code reviews on this repo. Configure here.

siyabendoezdemir and others added 3 commits October 10, 2026 00:13
…blocked quitting (D28)

Electron's default uncaughtException handler shows a modal error box that holds the
main thread until someone dismisses it, so Orbit couldn't quit. Under Xvfb nobody
dismisses it: e2e app.close() hung, then Playwright's worker teardown, which failed
whole jobs (macOS CI on PR #7 and main, three local full runs). A user would have
seen the box and an app that wouldn't quit.

- src/main/index.ts: log "[orbit] uncaught exception:" and carry on.
- tests/e2e/helpers.ts: app.close() fails a test whose main process logged one
  (takeUncaught() claims one thrown on purpose) and kills an Orbit that still
  hasn't quit 15 s after being asked, so one stuck quit can't hang the run.
- core.spec.ts: [TB-22] a throw in a main-process timer is logged, Orbit quits and
  the session restores. Without the fix nothing is logged and the quit hangs until
  the harness kills it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… before the evaluate replies

fromMini sends a command from the Mini Window's own page. mini.control's close can
close that page before Playwright gets the evaluate's reply, which then fails with
"Target page, context or browser has been closed" (MW-10, once in a full run). The
helper now ignores that error only when the page really closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… growing in

A pane added a moment ago springs open from zero width (SV-26), so its first
bounding box can be empty. The poll now retries instead of failing at once.

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

cursor Bot commented Oct 10, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: bfd72d5f-77f3-436b-83d1-3b65c22c88a0)

…de-out threw and blocked quitting (D28)

The strip fades out for 120 ms before its view hides. When its window closed
during the fade (as happens when Orbit quits right after a pane strip hid), the
fade's timer still ran on a view whose page was gone: view.webContents was
undefined, and reading isDestroyed() threw. Nothing caught that, and Electron's
modal error box then held the main thread, so Orbit never quit. This is the
exception behind the SV-03 hangs: 15 repeats of SV-03 with the D28 check logged
it once.

The window's closed handler now clears the strip's show, hide and fade timers,
and every check of the strip's view also handles a view whose webContents is gone.

Test: "[SV-12] a window that closes while its pane strip fades out closes cleanly
(D28)". It fails without the fix (3/3: the TypeError above) and passes with it.

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

cursor Bot commented Oct 10, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: 6a14680b-278f-4e9b-ac0e-001e3c849c28)

@siyabendoezdemir siyabendoezdemir changed the title Main-process exceptions no longer block quitting (D28); two test races Main-process exceptions no longer block quitting (D28): the check, the strip's fade timer, two test races Oct 10, 2026
@cursor

cursor Bot commented Oct 10, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: 1a54cc4f-a258-4fdb-817e-425b102e50fe)

…#11 review M1)

checkQuit kept only the last 4,000 characters of stderr before it looked
for the "[orbit] uncaught exception:" marker. An exception logged in one
longer write (a big message, a cause chain, an AggregateError) lost its
marker to the trim: the test passed and takeUncaught() returned ''.

The scan now looks for the marker in the whole chunk first, keeps up to
4,000 characters from it, and otherwise carries only a marker-length tail
so a marker split across two writes is still found.

New TB-22 test: a main-process timer throws a 5,000-character message.
It fails without the fix (takeUncaught() returns '') and passes with it,
3/3. core + split + smoke e2e: 62/62.

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

cursor Bot commented Oct 10, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: 40bc7f91-adcb-4857-8fb9-37735711fe79)

…Ubuntu 26 (Oct 19)

GitHub's notice says `ubuntu-latest` becomes Ubuntu 26 from 2026-10-19.
The e2e job installs xvfb, libgbm1 and libasound2t64 with apt, which is
only known to work on 24.04, the image `ubuntu-latest` runs today. So
nothing changes until that date; after it, the jobs stay on 24.04 until
someone checks 26 on purpose. CI-only change (D18).

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

cursor Bot commented Oct 10, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: f02846ec-ee67-4fa7-988b-19a1e1b95054)

siyabendoezdemir added a commit that referenced this pull request Oct 10, 2026
…cks apps that quit on their own, and samples a stuck main thread on macOS

Follow-ups F1, F2 and F4 from PR #11's review (D28).

- F1: Playwright does work of its own before it asks Orbit to quit
  (5 s after a failed test, measured by the reviewer), and the 15 s
  clock used to include it. In e2e mode main now logs
  "[orbit] quit requested" in before-quit; Orbit gets 15 s from that
  line, or 35 s from app.close() if the line never comes. The failure
  says which.
- F2: an app that quits on its own (its last window closed) had no
  app.close() to check it. It is now remembered when it closes, and
  the next launchOrbit() checks it.
- F4: macOS has no /proc, so a kill said "main thread: unknown". It now
  runs `sample <pid> 1` and reports the main thread's deepest frames,
  with the full sample's path. Not run on macOS yet; any error still
  falls back to "unknown".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
siyabendoezdemir added a commit that referenced this pull request Oct 10, 2026
…thing handled in main

Electron only prints a warning for an unhandled rejection in the main process,
so the D28 check never saw one. Main now logs them with the same marker as an
uncaught exception, so the check fails the test that caused it.

The new TB-22 test fails without the logging (received "") and passes with it.

PR #11 review F3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@siyabendoezdemir
siyabendoezdemir changed the base branch from lead/polish2 to main October 10, 2026 04:57
@siyabendoezdemir
siyabendoezdemir merged commit eea7a92 into main Oct 10, 2026
1 of 4 checks passed
siyabendoezdemir added a commit that referenced this pull request Oct 10, 2026
Conflicts:
- Sidebar.tsx: the update banner and media's tray both sit above the Space switcher, the banner first.
- rpc.ts: RendererView takes both 'pip' and 'onboarding'.
- FEATURES.md: regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
siyabendoezdemir added a commit that referenced this pull request Oct 10, 2026
docs/ARCHITECTURE.md §9: main's E2E bullet (dead proxy, deferred evaluates,
the quit check) with this branch's merge-gate bullet in place of the CI one.
FEATURES.md regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant