You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Electron ESM emits ready only after the main module finishes evaluating. Maka's main entry ran await resolveDesktopStorageRoot(...) in the top-level module-evaluation chain; on a storage-root identity conflict, confirmDesktopStorageRootRepair called await app.whenReady() — a guaranteed deadlock (module evaluation waits on ready, ready waits on module evaluation). Any user hitting the repair dialog got a silently hung process with no window.
Verified experimentally on Electron 43.1.1: ready fires only after module evaluation completes, and dialog.showMessageBox throws before ready, so the whenReady await is not removable — the check must leave the top-level chain.
Fix
Split the entry into a thin pre-ready main.ts and a boot.ts loaded via dynamic import inside the app.whenReady() callback:
main.ts (43 lines): app.setName, E2E userData redirect, single-instance lock (now an explicit if/else so the losing process exits without evaluating the boot chain), then whenReady().then(() => import('./boot.js')) with a showErrorBox fatal path (suppressed under isolated E2E so a failure exits fast instead of hanging on a modal).
boot.ts: the unchanged startup chain (root-identity check → stores → IPC → lifecycle) as module-level code, now evaluated after ready. The "root-identity check before any store/db write" ordering is preserved.
confirmDesktopStorageRootRepair: drops await app.whenReady() for an if (!app.isReady()) throw assertion (ready is guaranteed by the boot contract).
Shared E2E switches moved to startup-context.ts.
Reviewed independently by Claude Opus and Codex (consult) before implementation; both rejected the minimal "fire-and-forget gate" variant as a silent integrity hole and recommended this thin-entry + dynamic-import shape.
Validation
New E2E regression storage-root-conflict.spec.ts: seeds a valid marker, corrupts its dev, launches without a fixture, asserts the app parks at the modal repair dialog (the only accepted success signal — the dialog can only appear after ready, since the whole boot module runs inside the whenReady callback, so it simultaneously proves ready was reached and that the root-identity gate is holding) and writes nothing (no SQLite) before the user answers. Fails on main (deadlock) and would fail if the gate were ever removed; passes here. The observable form of "dialog is open" is a CDP evaluate that never settles, because the macOS modal loop stops answering evaluation.
main-process unit tests: 1314 pass. tsc, biome lint, format all clean.
Existing E2E paths verified: normal launch (send-message) and e2e-fixture seeding (scroll-geometry long-transcript).
Notes
ready no longer waits on the login-shell PATH probe (resolveShellEnv moved into boot), an incidental latency improvement.
Per review: console allow-list extended to boot.ts (CI test:dist gate), E2E success signal tightened to "parked at dialog" only, E2E fatal path no longer shows a modal, and stale main.ts references in comments updated to boot.ts.
Electron ESM emits `ready` only after the main module finishes
evaluating, so a top-level `await app.whenReady()` in the startup chain
deadlocks: module evaluation waits on ready, ready waits on module
evaluation. The storage-root repair dialog hit exactly this — any
root-identity conflict hung the process silently with no window.
Split the entry: main.ts now does only pre-ready work (setName, E2E
userData redirect, single-instance lock with a proper return) and
dynamic-imports boot.ts inside the whenReady callback. boot.ts keeps the
whole startup chain (root-identity check, stores, IPC, lifecycle) as
module-level code after ready, so the check still precedes every store
and db write and confirmRepair no longer needs whenReady at all.
Also:
- fix losing-second-instance exiting without returning, so it never
touches shared state (was opening SQLite before exit)
- surface fatal boot errors via showErrorBox instead of a silent exit
- E2E regression test: conflicting storage root reaches ready with the
repair dialog open and writes nothing before the user answers
Review (Claude Opus + GPT-5.6-sol, independent) findings:
- CI gate: check-console allow-list only covered main.ts; the 7 console
sites moved into boot.ts with the startup chain, so 'test:dist' failed
the audit. Allow boot.ts and refresh the stale main.ts reason.
- E2E: the regression test claimed a '[startup] app ready' console signal
that nothing consumed; what actually passed was a 1s CDP-timeout
heuristic, which could false-positive on any slow/stuck main process.
Tighten to accept ONLY 'parked at the modal repair dialog' as success:
the dialog can only appear after ready (whole boot module runs inside
the whenReady callback), so it simultaneously proves ready + gate
holding, and a deadlocked process or a removed gate both fail.
- Fatal path: suppress showErrorBox under isolated E2E (same reasoning as
the fixture-fatal path in boot.ts) so a boot failure exits fast instead
of hanging on a modal until test timeout.
- Comments: update stale 'main.ts' references to boot.ts where they name
the startup chain's home.
Not this branch's change. #1880 added a `check-console.mjs` allowlist entry on
one line that Biome wraps across four, so `format:check` — and with it the whole
`typecheck` job — has been failing on main since that merge, for every branch.
Fixing it here because this PR cannot go green without it. It is `biome format
--write` on that one file and nothing else.
…nal) (#1887)
* fix(scripts): format check-console allow-list entry
The PR1880 entry for main.ts exceeded the line width; biome format
required splitting it. format:check was failing CI on main.
* fix(desktop): make storage-root-conflict e2e signal platform-independent
The regression test for the ESM startup deadlock treated 'CDP evaluate
never settles within 1s' as the proof that the repair dialog was open.
That holds only on macOS, where modal loops block CDP evaluation; on
Linux (CI) the modal keeps answering evaluation, so the test failed even
though the app parked correctly — and a deadlocked main process would
have been accepted as a pass.
Replace the heuristic with an explicit contract: boot.ts prints
'[storage-root] root-identity conflict; parking at repair dialog'
synchronously before the modal (printed only after ready, only when the
gate fired), and the test waits for that console event. The workspace
write-free assertion is unchanged. Deadlock and gate-removal both never
print the signal, so both still fail the test on every platform.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Electron ESM emits
readyonly after the main module finishes evaluating. Maka's main entry ranawait resolveDesktopStorageRoot(...)in the top-level module-evaluation chain; on a storage-root identity conflict,confirmDesktopStorageRootRepaircalledawait app.whenReady()— a guaranteed deadlock (module evaluation waits onready,readywaits on module evaluation). Any user hitting the repair dialog got a silently hung process with no window.Verified experimentally on Electron 43.1.1:
readyfires only after module evaluation completes, anddialog.showMessageBoxthrows beforeready, so thewhenReadyawait is not removable — the check must leave the top-level chain.Fix
Split the entry into a thin pre-ready
main.tsand aboot.tsloaded via dynamic import inside theapp.whenReady()callback:main.ts(43 lines):app.setName, E2E userData redirect, single-instance lock (now an explicit if/else so the losing process exits without evaluating the boot chain), thenwhenReady().then(() => import('./boot.js'))with ashowErrorBoxfatal path (suppressed under isolated E2E so a failure exits fast instead of hanging on a modal).boot.ts: the unchanged startup chain (root-identity check → stores → IPC → lifecycle) as module-level code, now evaluated afterready. The "root-identity check before any store/db write" ordering is preserved.confirmDesktopStorageRootRepair: dropsawait app.whenReady()for anif (!app.isReady()) throwassertion (ready is guaranteed by the boot contract).startup-context.ts.Reviewed independently by Claude Opus and Codex (consult) before implementation; both rejected the minimal "fire-and-forget gate" variant as a silent integrity hole and recommended this thin-entry + dynamic-import shape.
Validation
storage-root-conflict.spec.ts: seeds a valid marker, corrupts itsdev, launches without a fixture, asserts the app parks at the modal repair dialog (the only accepted success signal — the dialog can only appear after ready, since the whole boot module runs inside thewhenReadycallback, so it simultaneously proves ready was reached and that the root-identity gate is holding) and writes nothing (no SQLite) before the user answers. Fails onmain(deadlock) and would fail if the gate were ever removed; passes here. The observable form of "dialog is open" is a CDP evaluate that never settles, because the macOS modal loop stops answering evaluation.tsc, biome lint, format all clean.Notes
readyno longer waits on the login-shell PATH probe (resolveShellEnvmoved into boot), an incidental latency improvement.boot.ts(CItest:distgate), E2E success signal tightened to "parked at dialog" only, E2E fatal path no longer shows a modal, and stalemain.tsreferences in comments updated toboot.ts.main.