Repository navigation
CONTESTED: close/quit veto sequencing with two dirty windows #443
Description
Activity
- addedwayfinder:prototypeWayfinder prototype decisionWayfinder prototype decision
on Oct 6, 2026 - addedelectron-compatElectron compatibility program areaElectron compatibility program area
on Oct 6, 2026 Generated by an AI agent (Claude Code, Fable 5.1) on behalf of @0monish, 2026-10-06, under the map's execution doctrine. Statements are labelled FACT / INFERENCE / UNKNOWN by their author; nothing here authorizes implementation.
Decision packet
Decision: How a quit request (Cmd+Q, app.quit, quitAndInstall) is sequenced against per-window close vetoes so that before-quit, per-window close, window-all-closed and will-quit match Electron 44.4.5 with two vetoing windows, and which AppKit termination reply the host uses to do it.
Classification:
needs-experiment· Milestone (YAGNI test against the first proof):next· Reversible: Yes before F01-T3 (#451) lands; the choice is host-internal. After conformance entries for quit ordering land, changing the mechanism means re-recording them. · Owner: Quit ordering owner KEL-139 AC6 (Linear: Done, completed 2026-09-29) with KEL-96 (Linear: In Progress, assignee GYLDLAB, fetched 2026-10-06) for the existing ordered Quit; map tickets F01-T3 (#451) (#451) and PANEL-P2 (#419) (#419) consume it.Facts
- FACT: Electron v44.4.5 overrides NSApplication terminate: and calls Browser::Quit(); the code comment says termination proceeds if all windows close and a cancelled window close aborts it (shell/browser/mac/electron_application.mm at v44.4.5, fetched 2026-10-06).
- FACT: neither electron_application.mm nor electron_application_delegate.mm at v44.4.5 contains applicationShouldTerminate or any NSTerminate reply (grep of the fetched files, 2026-10-06).
- FACT: Browser::Quit sets is_quitting_ from the before-quit result, then calls NotifyAndShutdown if there are no windows, otherwise WindowList::CloseAllWindows (shell/browser/browser.cc at v44.4.5).
- FACT: CloseAllWindows snapshots the window list, reverses it on macOS, and calls Close() on every window without re-checking the quitting flag (shell/browser/window_list.cc at v44.4.5).
- FACT: a close-event preventDefault calls WindowList::WindowCloseCancelled, and Browser::OnWindowCloseCancelled then sets is_quitting_ to false (native_window.cc and browser.cc at v44.4.5).
- FACT: Browser::OnWindowAllClosed emits will-quit via NotifyAndShutdown when quitting, and otherwise emits window-all-closed (browser.cc at v44.4.5); app.md says that after Cmd+Q or app.quit the window-all-closed event 'would not be emitted'.
- FACT: AutoUpdater::QuitAndInstall emits before-quit-for-update and calls CloseAllWindows directly, without Browser::Quit (shell/browser/api/electron_api_auto_updater.cc at v44.4.5).
- FACT: Apple documents that returning terminateLater 'causes Cocoa to run the run loop in the NSModalPanelRunLoopMode until your app subsequently calls reply(toApplicationShouldTerminate:)' (developer.apple.com NSApplication.TerminateReply.terminateLater, JSON endpoint fetched 2026-10-06).
- FACT: draw.io sets cmdQPressed in before-quit on macOS and calls app.quit() from window-all-closed when that flag is set; it clears the flag when the user cancels the unsaved-changes dialog (corpus drawio-desktop @2edf9fb, src/main/electron.js).
- FACT: KEL-139 AC6 requires one ordered quit owner; no AppKit termination hook exists in the Keld host today (spec read and grep at b4b907c, 2026-10-06).
- FACT: F01-T3 (feat(app): cancelable two-phase quit — before-quit/will-quit preventDefault and host-initiated quit through the per-window close veto #451) (GitHub feat(app): cancelable two-phase quit — before-quit/will-quit preventDefault and host-initiated quit through the per-window close veto #451, two-phase quit) is blocked by F01-T1 (conformance(app): pinned v44.4.5 macOS cells for the first-proof app surface (ready, whenReady/isReady, session facts, single-instance verdict) with runner-asserted expected status #445) and F02-T3 (feat(window): macOS two-phase cancelable close — host holds tao's CloseRequested, facade close/preventDefault, destroy, tombstone-before-closed, three exits and no host timer #450); the first-proof denominator is install, activation and a 4-step workflow ending in a single-window close (publish_plan.json; panel synthesis).
- FACT (Linear, fetched 2026-10-06): KEL-139 Done; KEL-96 In Progress, assignee GYLDLAB; KEL-143 Backlog.
Inferences
- INFERENCE (from source, no fixture run): Cmd+Q with two dirty draw.io windows in Electron 44.4.5 yields before-quit, then close on the newer window, then close on the older window (both vetoed, which cancels the quit), then the unsaved-changes dialogs, then closed for each window as the app destroys it, then window-all-closed (because the quit was cancelled), then draw.io's own app.quit, a second before-quit, will-quit and quit.
- INFERENCE: this whole state machine (quitting flag, cancellation on any close veto, reverse-order close, window-all-closed suppressed while quitting) can live in the facade, because it needs only a host 'quit requested' event, the per-window close protocol and the existing ordered Quit call.
- INFERENCE: Electron does not use a delayed termination reply at all, so matching it does not require NSTerminateLater. A host that returns terminateLater sits in the modal-panel run-loop mode until it replies, which may starve host work scheduled only in the default mode. This is new primary evidence against the 'later reply' mechanism clause in PANEL-D5 (Close and quit vetoes are async AppKit hooks; deadline expiry keeps the window open #425).
- INFERENCE: the first proof does not exercise quit, so it can ship without this decision.
Unknowns
- UNKNOWN: whether the Keld host keeps servicing Bun replies, web view callbacks and sheet presentation while AppKit holds it in the modal-panel run-loop mode after terminateLater.
- UNKNOWN: behaviour on logout, restart and shutdown, where cancelling termination aborts the system action.
- UNKNOWN: the recorded Electron transcript; the ordering above is source-derived only.
Alternatives
Option Cost New invariant created Existing invariant at risk A. Mirror Electron: the host cancels the native terminate request and emits one quit-requested event; the facade runs Electron's quit state machine; process exit happens only through the existing ordered Quit call. The facade must reproduce the quitting flag and its cancellation rule exactly; logout/shutdown handling needs its own decision. A native terminate request never terminates the host directly; only the ordered Quit call does. None identified; consistent with KEL-139 AC6 one quit owner. B. Host returns terminateLater and replies after the per-window close sequence (PANEL-D5 (#425)'s current mechanism). The host runs in the modal-panel run-loop mode for the whole unsaved-changes interaction; Electron does not do this, so interleaving has no oracle. The host may hold a pending AppKit termination across app-owned dialogs. KEL-142 rule that no UI thread blocks on Bun, if default-mode work stalls in that run-loop mode. C. Host serializes close requests per app and delivers them after any modal returns, recorded as a divergence (the panel's fallback). Event order differs from Electron when two windows veto; draw.io's second dialog timing changes. Close events are serialized across a blocking dialog. Electron ordering conformance for quit. Recommendation
Prefer A, and settle the A-versus-B host mechanism with a reduced harness when F01-T3 (#451) is scheduled: (1) an Electron 44.4.5 fixture with two windows that veto close, driven through Cmd+Q, app.quit and quitAndInstall, recording the event transcript to confirm the source-derived order; (2) a small objc2 host harness with two arms, terminateCancel plus later programmatic terminate versus terminateLater plus reply, each recording whether a default-mode run-loop source, a web view script callback and a window sheet are serviced while termination is pending. Exit: arm chosen where all three are serviced and the facade transcript equals the Electron transcript; if neither host arm can match, fall back to C with a sequence test. Keep this out of the first proof.
Falsifier: An Electron 44.4.5 transcript that differs from the source-derived order (for example, window-all-closed not emitted after a vetoed Cmd+Q), or a harness run showing terminateCancel cannot be re-driven to a clean ordered Quit.
Missing evidence: The Electron transcript for the three quit entry points with two vetoing windows; host observations under terminateLater; the logout/shutdown path.
Next action: Owner: map orchestrator. Post the source-derived Electron quit state machine and the two-arm harness on GitHub #443, add the terminateLater doc sentence to #425 as new evidence against its 'later reply' clause, and mark #443 as not blocking the first proof. First check: #443 lists both host arms and their three observables, and #451 references it instead of PANEL-P2 (#419) for quit ordering.
- addedmilestone:nextNeeded after the first proof (Zettlr or strict profile)Needed after the first proof (Zettlr or strict profile)
on Oct 6, 2026
Parent map: #391 · Unit: perspective panel (8 isolated personas → cross-critique → 3 judges) · Wayfinder type:
prototypeTracker of record: this GitHub issue (decision ticket; claim by self-assignment). Linear reference: KEL-237 (In Progress) carries a link here.
Question
CONTESTED: close/quit veto sequencing with two dirty windows
Current position (contested — see both sides)
Quit does not route through windowShouldClose, so a host that models quit as per-window close CALLs may mis-order before-quit / window closes / will-quit relative to Electron (and quitAndInstall inverts before-quit). Settle with a small objc2 harness + Electron 44.4.5 transcript diff; fallback is serialized close requests with ▲.
Evidence
Context
Raised by the eight-persona perspective panel (isolated positions → cross-critique → three judges); judge extracts and persona positions are on the research branch (
wayfinder/electron-compat/panel/).