Repository navigation
Stop CodexHub-managed Gateway when the app fully exits #112
Description
Reporter evidence
A user reports that after completely exiting CodexHub, its Gateway remains running. The expected result is that a Gateway started or restarted by the current CodexHub session shuts down with an orderly full exit, rather than being left running.
Problem
On the current main and dev branches, the tray Exit action calls app.exit(0) directly, while the explicit Stop Gateway action calls the existing ownership-aware proxy::stop() path. The application-update path similarly calls app.restart() directly. The Gateway is started as a child process and can therefore outlive the desktop app unless an orderly terminal path stops it first.
Closing the main window is intentionally different: it hides CodexHub to the system tray and must keep the Gateway available.
Outcome
Before every orderly terminal CodexHub action, stop only a Gateway managed by the current CodexHub session through the existing bounded, ownership-verified stop path. This covers Gateways started automatically on launch and those started or restarted from this app session. A full exit must not leave such a Gateway running; a window close to tray must not stop it.
Scope and safety invariants
- Cover tray Exit and the app-update restart path, plus any other existing orderly terminal app path found during implementation.
- Record session ownership only after a successful CodexHub-initiated start or restart, tied to the existing verified process identity.
- If a Gateway was already running before the current CodexHub session, do not stop it on exit.
- If the recorded process has been replaced, restarted externally, or no longer matches its verified identity, clear/refuse ownership rather than killing an unrelated process.
- Reuse the existing
proxy::stop()safeguards (PID, executable, and port verification); do not introduce broad process-name or port-based termination. - Cleanup failure must be bounded, produce sanitized diagnostics, and must not hang application exit indefinitely.
Acceptance criteria
- From one CodexHub session, manually start the Gateway, then choose tray Exit; the session-owned Gateway is stopped through the verified stop path before the app exits.
- With
auto_start_gatewayenabled, a newly auto-started Gateway is stopped on a true full exit. - With
auto_start_gatewaydisabled, or when a Gateway predates the current CodexHub session, full exit does not stop that pre-existing Gateway. - Closing the main window to the system tray leaves a session-owned Gateway running.
- An update-triggered app restart performs the same bounded cleanup before the old app process restarts; any Gateway subsequently started by the new session is distinct from the old session's process.
- Repeated start/exit, second-instance, Gateway-restart, changed-process-identity, and stop-failure paths do not double-stop, kill an unrelated process, or leave stale ownership state.
- Deterministic Rust lifecycle/state-machine tests cover pre-existing, manual start, auto-start, failed start, externally replaced process, tray close, full exit, update restart, and failed stop cases.
- A packaged Windows smoke test proves both behaviors: tray-close retains the Gateway; tray Exit makes the current session's Gateway health endpoint unavailable and its recorded process exit.
Non-goals
- Guaranteeing cleanup after an OS crash, power loss, Task Manager kill, or other forced process termination.
- Stopping a Gateway that this CodexHub session did not start or cannot verify as owned.
- Changing normal manual Gateway start/stop UX, except where needed to maintain accurate session ownership or report a bounded cleanup failure.
- Replacing the existing ownership-verified Gateway stop model.
Verification
- Add targeted deterministic Rust lifecycle tests for the acceptance cases above.
- Run the repository-documented relevant Rust suite:
cd src-tauri; cargo test. - Perform the packaged Windows smoke test named in the acceptance criteria.
Expected hotset
src-tauri/src/main.rssrc-tauri/src/app_updates.rssrc-tauri/src/proxy.rs- Rust lifecycle state and tests adjacent to those modules
Dependencies
Blocked by #143. #143 is blocked by #139.
#143 owns the approved user-requested shutdown contract: close admission, cancel active upstream work, best-effort emit user_requested_shutdown, allow at most two seconds for close/flush, then force-terminate only the ownership-verified current-session Gateway. This issue consumes that result for tray Exit and update restart.
#139 owns concurrent start/stop/restart serialization, singleflight startup, and PID/UI/listener reconciliation. This issue continues to own tray Exit, update restart, close-to-tray preservation, and terminal cleanup of only the current session-owned Gateway.
The former GLM-5.2 priority path (#105 and #108) is complete and no longer blocks scheduling.
Execution contract
Execution-Contract: v3
Execution-Mode: paseo-agent
Agent-Role: implementation
Role-Category: impl
Integration-Branch: dev
Done-When: Tray Exit and update restart stop only the current-session-owned Gateway through the existing ownership-verified bounded stop path before the app exits; close-to-tray keeps it running; pre-existing or identity-mismatched Gateways are never stopped; deterministic Rust lifecycle tests and the packaged Windows smoke pass.
Verification-Class: strict
Verification-Commands: cd src-tauri; cargo test --locked; cd src-tauri; cargo clippy --locked --all-targets -- -D warnings
Manual-Evidence: Packaged Windows smoke test: from one app session start the Gateway, close the window and verify health remains available, then choose tray Exit and verify the session-owned Gateway health endpoint is unavailable and its recorded process has exited.
Architecture-Decision: resolved
Review-Owner: orchestrator
Activity
[-]Stop session-owned auto-started Gateway when CodexHub fully exits[/-][+]Stop CodexHub-managed Gateway when the app fully exits[/+]NOirBRight commentedon Jul 16, 2026
Installer file-lock impact reproduced during 0.1.5 replacement
While replacing the prior Debug installation with the signed Normal 0.1.5 installer, the CodexHub desktop process was absent but its embedded-Python Gateway remained alive, owned port 9099, and had
<install-root>\python\_bz2.pydloaded. The installer then stopped with:Error opening file for writing: <install-root>\python\_bz2.pydA deterministic exclusive-open probe failed because another process held the file. After identity-verifying and terminating only that exact CodexHub Gateway child, the same probe passed, port 9099 had no listener, and the installer continued successfully through Retry.
This rules out a corrupt 0.1.5 package or file ACL as the observed cause and records a direct update/replacement impact of #112. The packaged Windows lifecycle acceptance should include one full-exit-then-replacement leg proving that no session-owned Gateway or embedded-Python file lock survives to block the installer.
NOirBRight commentedon Jul 19, 2026
Dispatched by campaign \c-016-lifecycle\ (orchestrator 3b6c57b6) after #143 merged (dev f8cae58). dispatch_id=\dispatch-issue-112-a1; worker=21f9bf33-b341-42d1-9f94-d7fc613e220e (codex/gpt-5.6-terra, thinking max, full-access); branch \work/issue-112-exit-cleanup\ from base f8cae58; PR target \dev; verification class strict (packaged Windows smoke executed in worker worktree: tray-close retains Gateway, tray Exit stops session-owned Gateway). Room: \gwo-c-016-lifecycle, START signal \d-112-a1-start-001.
NOirBRight commentedon Jul 19, 2026
[Coordinator handoff record — 2026-07-19]
Verified candidate, WAITING_INTEGRATION; recovery ownership transferred to successor Repository Coordinator
0269bc32-a266-498f-af6f-24877d8cf8ca.b4471bc8e5fcd8a9b33eacab024e9fdf306121f2on basef8cae58b(one commit:app_updates.rs,main.rs,proxy.rs+449/-120) — PR #112 Stop session-owned Gateway on terminal app exit #184 (OPEN, MERGEABLE, not draft).pass(room9ae4fc1b, signal d-review-112-a1-quality-result-001) + specpass(room1167d810, signal d-review-112-a1-spec-result-001); identical lock (candidate/base/diff/acceptance sha256 match); non-blocking notes only.be69cbb7(signal d-112-a1r-done-001).21f9bf33was archived prematurely at 2026-07-19T04:12:06.855Z; Campaign Orchestrator3b6c57b6archive was reported at 04:12:14Z, but Coordinator readback at handoff observes itidle(noarchivedAt). No COMPLETED / CAMPAIGN_CLOSED was posted; do not treat the campaign as normally closed.devonly — all other gates green.— Retiring Repository Coordinator
568ab644-bf73-42e6-8da5-a4fd07d75aec(campaign c-016-lifecycle)Merge pull request #184 from NOirBRight/work/issue-112-exit-cleanup
NOirBRight commentedon Jul 19, 2026
Integrated via PR #184 into
devas merge commiteff1d99c81bed96f6b0ce421d6f2900938a08300(2026-07-19T04:24:22Z).Recovery readback:
b4471bc8e5fcd8a9b33eacab024e9fdf306121f2;The premature Paseo archive was recovered without creating a successor Worker or changing the candidate. Closing the Issue on the verified
devintegration.