Skip to content

Cancel active Gateway work within two seconds on explicit lifecycle stops #143

Closed

Description

@NOirBRight

Reporter evidence

During a controlled cleanup on 2026-07-16, an old Gateway process was intentionally terminated while at least one Official Responses stream was still active. The Gateway event stream ended mid-request without a terminal record. Codex Desktop then emitted exactly five stream-disconnect retries across three active Tasks before the Gateway returned.

This was not an unexplained network reset and Computer Use was not the initiating cause. It is deterministic evidence that terminating the local Gateway while it owns active streams converts one lifecycle action into repeated client reconnects.

A separate same-session observation reproduced the duplicate-start race already owned by #139; this Issue does not own startup serialization.

Accepted lifecycle decision

The prior long-drain/no-forcing contract is superseded. On 2026-07-16 the maintainer selected decisive interruption for explicit user lifecycle actions:

  1. Immediately stop admitting new requests.
  2. Immediately cancel active upstream work.
  3. Best-effort emit a sanitized user_requested_shutdown terminal/cancellation outcome downstream.
  4. Allow at most two seconds for cancellation, connection close, and diagnostic flush.
  5. After two seconds, forcibly terminate only the ownership-verified Gateway belonging to the current session.
  6. Warn before the action that active Codex Tasks may be interrupted; do not show a post-timeout Wait/Cancel modal.

Problem

CodexHub has no single cancellation handshake between the Rust lifecycle controller and the Python Gateway request plane. Explicit Stop, Restart, full Exit, update install, and release-channel transition can therefore accept new work during retirement or turn an intentional shutdown into an unexplained network reset and retry burst.

#139 will serialize lifecycle operations and publish the authoritative process identity. #112 owns full Exit/update cleanup, but both need the same bounded request-plane cancellation result.

Outcome

Implement one fast, ownership-safe user-requested shutdown contract used by every explicit Gateway-retiring action. The contract prioritizes decisive completion of the user's action over finishing active model work, while making the interruption classifiable downstream and never killing an unverified process.

Scope and safety invariants

  • Expose a local authenticated/control-plane-only shutdown/cancel operation with authoritative admission and active-request state.
  • Route explicit Stop, Restart, full Exit, update install, and Stable/Developer channel transition through the same result after Serialize Gateway lifecycle operations and prevent duplicate startup processes #139 acquires the lifecycle boundary.
  • Close admission before issuing upstream cancellation. Requests arriving afterward receive one documented sanitized local outcome and never begin upstream work.
  • Propagate cancellation to Official and third-party upstream transports without changing Provider routing, protocol selection, or normal retry budgets.
  • Best-effort send a downstream terminal/cancellation result classified as user_requested_shutdown before closing the connection. Never include prompts, responses, credentials, Provider secrets, or Task identifiers.
  • Bound cancellation, downstream close, and diagnostic flush to two seconds total from admission closure. Do not extend the bound per request or per connection.
  • If cooperative shutdown has not completed by the deadline, invoke Serialize Gateway lifecycle operations and prevent duplicate startup processes #139's ownership-verified force termination for only the current session's canonical Gateway identity.
  • A pre-existing, externally replaced, or unverifiable Gateway must not be force-killed; return a truthful ownership error instead.
  • Restart holds Serialize Gateway lifecycle operations and prevent duplicate startup processes #139's lifecycle boundary continuously through cancellation/force-close, old-identity reconciliation, replacement start, and publication of the replacement identity.
  • Present one pre-action warning that active Codex Tasks may be interrupted. Do not add a post-timeout Wait/Cancel decision or leave the Gateway running indefinitely.
  • Direct process termination, Task Manager, OS crash, and power loss remain outside this cooperative terminal-outcome guarantee.

Non-goals

Acceptance criteria

  • With zero active requests, each explicit retiring action closes admission, stops the verified session Gateway promptly, and reconciles listener/PID/UI ownership state.
  • With one or multiple active Official Responses streams, Stop immediately closes admission and requests upstream cancellation; downstream receives user_requested_shutdown when the connection still permits it.
  • The same contract applies to active third-party Responses work without changing that Provider's normal adapter behavior.
  • New requests after admission closure receive the documented user-requested-shutdown outcome and create no upstream request.
  • Cancellation/connection-close/flush gets one total two-second budget. A deterministic clock test proves no per-request multiplication or unbounded wait.
  • At the deadline, only the exact Serialize Gateway lifecycle operations and prevent duplicate startup processes #139-verified current-session Gateway is forcibly terminated; a replaced, pre-existing, mismatched, or unverifiable process is never killed.
  • A forced close is logged/classified as user_requested_shutdown rather than an unexplained upstream/network failure, without sensitive request or Task data.
  • Restart holds one lifecycle boundary, removes/reconciles the old identity, starts exactly one replacement, and publishes one matching child/listener/PID/UI identity.
  • Stop CodexHub-managed Gateway when the app fully exits #112 full Exit and update restart consume the same contract and do not leave the embedded-Python process or installer file locks behind.
  • Stable/Developer channel transition consumes the same contract before binary replacement.
  • The UI/tray shows one localized pre-action interruption warning and no post-timeout Wait/Cancel modal.
  • Deterministic tests cover zero/one/many requests, a request racing admission closure, cooperative cancel success, downstream already closed, timeout, forced termination, identity replacement, restart, full Exit, and update/channel transition.
  • A packaged Windows Debug smoke with long-running Official and third-party legs proves interruption is warned/classified, the retiring listener is gone after the two-second close/force budget, and Restart leaves exactly one reconciled replacement Gateway.

Relationships

Expected hotset

Verification

  • python -m pytest -q tests/test_proxy_shutdown.py
  • Focused Python request-admission/cancellation tests
  • cd src-tauri; cargo test --locked
  • cd src-tauri; cargo clippy --locked --all-targets -- -D warnings
  • cd frontend; npm run build; npm run test:ui-contract if frontend/tray presentation changes
  • python scripts/report_quality_gates.py (report-only; non-blocking)

Execution contract

Execution-Contract: v3
Execution-Mode: paseo-agent
Agent-Role: implementation
Role-Category: impl
Integration-Branch: dev
Done-When: Every explicit Gateway-retiring action (Stop, Restart, full Exit via #112, update install, channel transition) routes through one ownership-safe shutdown contract: admission closes first, active upstream work is cancelled, downstream receives a sanitized user_requested_shutdown outcome when deliverable, the whole close/cancel/flush completes within two seconds, and only the #139-verified current-session Gateway is force-terminated at the deadline; deterministic tests and the packaged Windows Debug smoke pass.
Verification-Class: strict
Verification-Commands: python -m pytest -q tests/test_proxy_shutdown.py; focused Python request-admission/cancellation tests; cd src-tauri; cargo test --locked; cd src-tauri; cargo clippy --locked --all-targets -- -D warnings; frontend build/UI-contract checks if frontend changes; python scripts/report_quality_gates.py (report-only)
Manual-Evidence: Packaged Windows Debug with separate long-running Official and third-party legs: invoke Stop/Restart, prove the pre-action warning and user_requested_shutdown classification where deliverable, prove the retiring listener/process is gone after the two-second close/force budget, and prove Restart leaves exactly one reconciled replacement identity.
Architecture-Decision: resolved
Review-Owner: orchestrator

Activity

  1. added
    bugSomething isn't working
    needs-triageMaintainer needs to evaluate this issue
    ready-for-agentFully specified, ready for an AFK agent
    wayfinder:taskWayfinder implementation task
    and removed
    needs-triageMaintainer needs to evaluate this issue
    on Jul 15, 2026
  2. changed the title [-]Quiesce active Responses streams before normal Gateway lifecycle stops[/-] [+]Cancel active Gateway work within two seconds on explicit lifecycle stops[/+] on Jul 16, 2026
  3. self-assigned this
    on Jul 18, 2026
  4. NOirBRight commented on Jul 18, 2026

    @NOirBRight
    OwnerAuthor

    Dispatched by campaign \c-016-lifecycle\ (orchestrator 3b6c57b6). dispatch_id=\d-143-gateway-shutdown; worker=d2a9e493-f225-45f3-b40a-b5be84152809 (codex/gpt-5.6-terra, thinking max, full-access); branch \work/issue-143-gateway-shutdown\ from base b4157b3; PR target \dev; verification class strict (packaged Windows Debug smoke executed in worker worktree). Room: \gwo-c-016-lifecycle.

  5. NOirBRight commented on Jul 18, 2026

    @NOirBRight
    OwnerAuthor

    Recovery dispatch attempt 2 by campaign \c-016-lifecycle\ (orchestrator 3b6c57b6, coordinator adjudication \c-016-lifecycle-adjudication-143-151-001). dispatch_id=\dispatch-issue-143-a2; worker=b2cd7a0a-b572-4739-97bf-c10b554f149f (codex/gpt-5.6-terra, thinking max, full-access); attached to existing worktree on branch \work/issue-143-gateway-shutdown; inherits attempt-1 WIP (commits 5686234+33adb3b5 + 5 uncommitted files), commits it durably first, refreshes base onto e56c4c6; PR target \dev; verification class strict (packaged Windows Debug smoke executed in worker worktree). Room: \gwo-c-016-lifecycle, START signal \d-143-a2-start-001.

  6. NOirBRight commented on Jul 18, 2026

    @NOirBRight
    OwnerAuthor

    Recovery dispatch attempt 3 by campaign \c-016-lifecycle\ (orchestrator 3b6c57b6; maintainer decision 2026-07-18: archive-then-attempt-3). Predecessor b2cd7a0a archived (guard-eligible, worktree preserved). dispatch_id=\dispatch-issue-143-a3; worker=da058135-c17c-4a60-9aaf-fd37323a4c1e (codex/gpt-5.6-terra, thinking max, full-access, fresh Computer-Use session); re-attached existing worktree on branch \work/issue-143-gateway-shutdown\ at head 23c8265 (== origin; base e56c4c6 merged); remaining strict: clippy, sanitized strict re-run on final head, packaged Windows Debug smoke completion, then PR targeting \dev. Room: \gwo-c-016-lifecycle, START signal \d-143-a3-start-001.

  7. added a commit that references this issue on Jul 19, 2026
    f8cae58
  8. NOirBRight commented on Jul 20, 2026

    @NOirBRight
    OwnerAuthor

    Released in CodexHub 0.1.6. The implementation merged to dev in PR #183 and was promoted to main by release PR #189 at cc9df19; all required #189 checks passed. Closing the stale open ticket and clearing dispatch-ready state.

  9. removed their assignment
    on Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingwayfinder:taskWayfinder implementation task

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions