Skip to content

fix(lifecycle): preserve services when Quit is cancelled - #945

Merged
Juliusolsson05 merged 7 commits into
mainfrom
fix/quit-service-lifecycle
Sep 20, 2026
Merged

Juliusolsson05 merged 7 commits into
mainfrom
fix/quit-service-lifecycle

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Cancelling Quit can leave Agent Code open after workflows, MCP, LSP, remote access and dictation have already stopped. Move those disposals into one committed-quit composition: an editor veto preserves services; a committed quit joins execution stops, startup, supporting services and admitted write tails before releasing the process lock.

The terminal gate publishes its exact drain before calling shutdown code. Repeated requests join it, completed stages are retained across retry, and failed owners hold the exit boundary. All window creation routes reject after commitment. Partial startup publishes workflow ownership before initialization and retains the lock through cleanup. Dictation cancels owned batch HTTP, joins admitted handlers/hotkey work and cancels optional previews without awaiting an abandoned stop promise. Remote disposal joins its enable/disable queue and retains a failed server owner.

Validation: 58 distinct focused unit cases across seven files, nine remote system cases and one real-editor-guard/application-composition renderer case; the final changed-path lane passed 21 cases. Full typecheck plus final-source incremental typecheck, contract and seven pinned-checkout checks passed. The application build/entrypoint verification (npm run test:package) passed on the final source 3d382fb7, and both CI gates (quality and minimum-Node fixture) are green on that head. The shutdown diagram was regenerated, verified and visually inspected. No live user app or microphone/provider session was used; system tests own their state/transports.

The first bounded B02 repair is independently based on 115e26fc. Its focused plan is the branch's first commit (841d0949); implementation is e9b88bf5 plus cancellation review 3d382fb7. No provider package pin changed and no signed release was produced.

Fixes #941. Fixes #942. Refs #919, #943 and #918.

B02 remains open: revision-bound editor votes, participant revalidation and final save/control-operation receipt frontiers are not implemented here. Write-tail settlement does not imply successful retries or fsync. Current support-service disposal promises do not manufacture native process-exit evidence. The pinned preview cancel API deliberately abandons a pending stop promise; the host cancels it and fences late observations. The follow-up control drain must include pre-receipt admission, nested waits/batches and internal completion writes, as recorded in #943 and the focused plan.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FcJNPX21SkzfKhRFxftriT

Juliusolsson05 and others added 4 commits September 19, 2026 18:31
Main gained three quit-time owners after this branch was cut, and each
had to be placed in the committed-shutdown composition rather than left
on before-quit, which runs before the renderer's unload vote:

- The extension runtime (#577). Its PAUSE stays on before-quit: it is
  the one preparation that must finish before windows unload, and it is
  reversible (Keep Editing resumes it). Its disposal is a new committed
  stage, `stopExtensions`, which runs beside sessions and workflows —
  extensions run user code — and only AFTER startup settles, because
  startup creates the runtime and an earlier stop would complete against
  nothing and keep that receipt.
- The performance monitor and trace drain (#958), with its 1.8s/2s
  bounded race, folded into the `performance` stage. Main also ran
  monitor/watchdog/performanceService stops in will-quit; those are
  already stages here, so the duplicates are dropped.
- tmux recovery now goes through restoration's canonical envelope
  decoder. A quit committed mid-read must not authorize killing tmux
  sessions, so the startup assertion moved inside the reader, where it
  becomes the decoder's read-failure path and withholds cleanup.

`workflowShutdownPromise`/`Complete` are gone with the before-quit
handler that used them; workflow stop is a committed stage.

The shutdown diagram was rewritten as a flowchart in main's current
convention (accTitle/accDescr/scope, #954) and says what the composition
actually does: the editor decides first and Keep Editing leaves every
service live; only a committed quit stops execution; a failed stop keeps
the app and its process lock. Re-rendered with the pinned toolchain;
every other SVG restored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The merge that placed main's extension runtime in the committed-quit
composition staged index.ts but not applicationShutdown.ts, so the
services object passed a stage the interface did not declare. Local
`tsc -b` was incremental and stale; CI caught it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codex review of #945. When a committed quit fails, will-quit has been
admitted and every window is gone, so the dialog is the only control the
application still has. It said "Quit again to retry" while offering one
button, "Keep Agent Code Open", and discarded the response. On macOS a
user can quit again from the Dock; on Windows and Linux the menu bar
belongs to a window, window creation is fenced after commitment, and
focusWindow has nothing to focus — so a transient stop failure stranded
the process and its state lock with no reachable action, even though a
second attempt would have succeeded.

The dialog now offers Retry Quit (default) and Keep Agent Code Open, and
calls app.quit() on a retry. Escape maps to "keep open", never a retry:
a retry can kill live sessions. Retrying is safe by construction —
applicationShutdown drops failed stages at the next drain and keeps the
completed ones.

It lives in its own module because index.ts cannot be imported by a test
(it builds the whole application on import), and this is behaviour rather
than presentation. Four tests drive it: the retry quits, waiting does
not, an aggregate failure reports every cause, and a dialog that throws
never escapes into the shutdown path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codex delta review of #945, reproduced against the real gate: the gate
clears its shutdown promise when a drain fails, so a second quit can
fail and report while the first dialog is still unanswered. That opened
a second dialog with its own live Retry — two ways to re-enter shutdown,
stacked over an application that has no windows left.

One presentation is held at module scope; a report arriving while it is
open resolves against it instead of opening another. Ownership is
cleared before a retry, so the quit it triggers can report its own
failure rather than be swallowed as a duplicate. The test holds the
first dialog unanswered, reports a second failure, and expects exactly
one showMessageBox; it fails without the guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Juliusolsson05
Juliusolsson05 merged commit d585625 into main Sep 20, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the fix/quit-service-lifecycle branch September 20, 2026 02:13
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.

bug(dictation): join pending operations before committed quit finishes bug(remote): join enable work and retain failed owners during disposal

1 participant