Feature checks passed, yet the final close failed. A rapid second request could destroy the parent while the first awaited storage. Fixing Northstar's close race required aligning parent window, child tabs, persistence and kernel lifetimes rather than merely hiding a button briefly.

Closing begins before the window can be destroyed

The close event prevents the default destruction. If quitting is already set, another request returns while the first sequence continues. The initial request marks quitting/closing and stops timers and controllers. A second click must not become a separate destruction path.

The original failure occurred while the first request awaited draft and tab storage. A click asking to close differs from the moment destruction is appropriate. This is more than duplicate-click suppression: it gives lengthy asynchronous cleanup one lifecycle. A hurried double request should be part of ordinary input conditions, not dismissed as an impossible user action.

Host disposal has its own ordering

BrowserHost.dispose captures drafts, marks closed and persists browser state before detaching the driver and closing live contents. Login retention, pending download-history writes and storage closure have their own steps. A vanished window alone does not prove persistence completed.

Ordinary close also avoids removing tabs after host closure or parent destruction, and disposal does not close already-destroyed contents again. This reduces late child-destroyed handlers touching a dead parent view. Closing one tab and terminating the application have different preservation responsibilities even when their methods appear near each other in the code.

Consider notebook cleanup and late status delivery

The close path calls notebook.shutdown and flush for each project because kernels, final outputs and sources have lifetimes different from the window. Read which asynchronous work is grouped and when destruction occurs rather than infer persistence from a progress message.

A finally on Promise.all is not a durability guarantee across every failure. Rejection of one member does not by itself establish completion or successful writes for all other cleanup. The supporting success evidence here concerns the normal rapid-close race test. It does not prove recovery under power loss, disk failure or every kernel combination. Those require additional failure-path checks.

Retain the same regression before and after the fix

The existing close-lifecycle test repeats rapid duplicate quit and restart three times in an independent profile. Each iteration collects main-process exceptions and compares exact tab IDs and draft content. A normal window close adds the seventh result. The question extends beyond process exit to retained work.

The historical 0.1.60 run failed with the destroyed-object error while 0.1.61 passed the same regression. That supports the packaged path and specified tabs/draft, not external publication or session-expiry behavior. Retaining the earlier failure explains the change and preserves the fact that twenty other feature checks passed while closing still failed.

Explain closed and saved as different outcomes

An accepted close response is not proof that every item restores on the next start. Evidence should connect to the same tabs and manuscript after restart. A useful waiting message explains which work is being settled without exposing unnecessary implementation terminology.

I neither closed the original app nor reran its lifecycle test for this article. Source and historical records support the account; the synthetic preservation module only compares projections. The reusable choice is to prevent a hurried second close from interrupting the first cleanup and to keep completion claims within observed scope. A convenient close action still needs an order that protects the person's work.