Skip to content

fix: stop Codex and OpenCode helper servers when Orca quits or crashes (STA-9254, 2 of 2) - #25753

Merged
brennanb2025 merged 51 commits into
mainfrom
brennanb2025/supervised-codex-opencode-probes
Oct 7, 2026
Merged

brennanb2025 merged 51 commits into
mainfrom
brennanb2025/supervised-codex-opencode-probes

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 14 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​896 $\color{#cf222e}{\Huge{\mathbf{−}}}$​17 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​879
Prod 7 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​382 $\color{#cf222e}{\Huge{\mathbf{−}}}$​95 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​287

ELI5

Before an OpenCode launch, Orca starts an OpenCode server for a few seconds to read which agent and models are configured, then stops it. That server never exits on its own. If Orca quit, crashed or was killed during those seconds, the server kept running forever, even with the real OpenCode CLI.

Codex has the same problem in a milder form. Orca briefly starts a Codex server to list Codex's models, to grant hook trust and to repair Codex's session index. While Codex rebuilds that index, Orca keeps a Codex server running for up to an hour. Each was a plain child of Orca, so a Codex server that was stuck kept running after Orca was gone.

Now each of these runs under the same supervisor native chat already uses. It is a small watcher process that stops the server, and anything it started, when Orca goes away.

What Changed

Before:

  • OpenCode launch preflight. Orca runs opencode serve, closes its input, reads /api/model, /api/agent, /api/model/default and /api/config, and then stops it, within a 10 s timeout. The server was started in its own process group and never exits when its input ends. A quit, crash or force-kill in that window left it running with nothing ever reaping it.
  • Short-lived Codex servers (codex app-server for the model list, hook trust grants and session-index repair). Orca counted on Codex exiting when its input closed. At the deadline it SIGKILLed only the top process and its direct children. A Codex server waiting on the network outlived Orca.
  • Codex index backfill. Orca keeps a read-only codex app-server alive for up to an hour while Codex rebuilds its index, with its input held open. A quit or crash left it running.

After (macOS and Linux):

  • All three are stopped when Orca quits, crashes or is killed, together with anything they started.
  • Codex gets the same close Orca gives its native-chat Codex connection: its input ends first, it gets 1 s to finish writing (its credentials file, its state database), and only then SIGTERM, a 3 s grace and SIGKILL, all aimed at its whole process group. That holds on a deliberate stop and when Orca dies.
  • The OpenCode preflight is stopped at once when Orca dies. Its own stop now kills the rest of the server's group as soon as the server exits on SIGTERM. Before, a helper the server had started that ignored SIGTERM was left running.
  • The preflight only uses what it read if the server is proven stopped. Otherwise the launch falls back exactly as it did before when a stop could not be proven.
  • Error messages are unchanged. A missing Codex binary is still classified as a missing binary in trust-grant telemetry, and no internal supervisor text reaches an error message.
  • Windows and WSL keep the same close: end of input, 1.5 s, then the whole tree is force-killed. A Codex session's close now runs through main's shared provider close instead of its own copy.

The plan, and which step this is:

  • Step 1, fix: stop Claude and agent helper processes when Orca quits or crashes (STA-9254, 1 of 2) #25715 (merged):
    • the supervisor's one-shot mode;
    • every agent CLI run through the source-control launcher: the Claude model list, model discovery, and commit-message / PR / branch-name generation;
    • one shared stop sequence;
    • closing each provider, when Orca dies, the way Orca itself closes it.
  • Step 2, this PR:
    • short-lived Codex app-server sessions;
    • the Codex backfill app-server;
    • the OpenCode launch preflight.

Mechanism:

  • Codex sessions (runCodexAppServerSession, codex/codex-app-server-session.ts). The callers are the Codex model-list probe, the hook trust grant and both session-index repairs. The orca CLI's hook commands use them too.
    • They now spawn through main's spawnManagedProviderProcess, the same managed process and close policy as native chat's Codex connection, so a session closes exactly the way that connection does.
    • Closing is managed.close(): end input, give the supervisor its full stop time, then tear the tree down. A session past its deadline is wedged, so it also gets SIGTERM straight away.
    • A supervisor that could not start Codex reports it on one marked line. The session maps that back to the spawn error and shows its stderr through the shared display formatter.
  • Codex backfill (codex/codex-state-db-backfill-recovery-process.ts, split out of the recovery module). Spawned in the supervisor's session lifetime with an end-of-input close request. Its stop goes through the shared stopSupervisedChildProcess.
  • OpenCode preflight (opencode/opencode-launch-model-context.ts).
    • It runs opencode serve in the one-shot lifetime. The preflight closes the server's input right away, and in a session that would stop it a second later.
    • It is stopped through stopSupervisedChildProcess (SIGTERM to the supervisor).
    • It counts the server as stopped only when the supervisor itself ended the group, by its own stop or by relaying the server's exit. A forced teardown proves nothing, because it reaches the supervisor's tree but not members left in the server's own group. An exit of 1, which a failed group reap also produces, counts as unproven too.
  • Crash breadcrumbs. A forced teardown is filed under its caller's own site: codex-app-server-session, codex-state-db-backfill-recovery or opencode-launch-model-preflight.
  • The CLI build. The sessions also run in the orca CLI, so config/tsconfig.cli.json lists the shared teardown and the modules it imports. Without them the CLI typecheck fails with 16 missing-file errors.

Why

The failure is the same one step 1 fixes: these servers have no owner that outlives Orca's own timers. The supervisor already gives native chat's Codex connection that guarantee in production, so these reuse it, with the same close request, rather than adding another mechanism.

The OpenCode preflight could not just rely on end of input. In OpenCode's source, opencode serve ends in a wait that never returns and does not watch its input. So the preflight needs the one-shot lifetime, where only Orca's death or an explicit stop ends it.

Alternatives considered:

  • A kill list at quit, or reaping recorded servers at the next launch. Both are rejected in step 1. The first misses a crash. The second leaves servers running until then and adds persisted state for something that can be derived.
  • Keeping the preflight's own stop wrapper. It counted a forced teardown, or a closed pipe, as proof that the server was gone. Using the shared stop removes a second copy of the ladder and makes the proof strict.

Differences from the common pattern:

  • Intended:
    • The backfill's stop is now the common close sequence. It ends input, waits 1 s, sends SIGTERM to the group, waits 3 s, then sends SIGKILL to the group, with a tree teardown only after 5.5 s. Before, it was SIGTERM to the top process at once, then SIGKILL to that one process at 5 s. So SIGKILL now comes at 4 s instead of 5 s, but it reaches the whole group, and the drain-first close matches how Orca closes its native-chat Codex connection.
    • One supervisor per server, not a long-lived host process per provider, for the reasons in step 1.
    • A session past its deadline gets SIGTERM straight away. It is wedged, so the input-end grace would only add a second.
    • The preflight trusts only a proven stop. A forced or unproven stop returns no context, which was already the preflight's behaviour for an unverified stop.
  • Temporary:
    • Windows and WSL get no crash coverage yet: STA-9579.
    • Still unsupervised: the Codex rate-limit probe (STA-9576) and the Codex login session (STA-9578).
    • Pre-existing, not changed here: current OpenCode source prints opencode server listening on …, but the preflight looks for a line starting server listening on. Whether OpenCode 2.0.16, the only version that runs the preflight, prints the new line is not verified here. A release with the new line would make every preflight time out: STA-9584.

Cost (macOS, stand-ins):

  • One supervisor process per server. Step 1 measured that supervisor at about 16-17 MB of physical memory, for the server's lifetime.
  • OpenCode preflight: 2001, 2010 and 2149 ms with the supervisor against 1952, 1970 and 2264 ms without it. These are the same run-to-run spread, with a stand-in that starts listening after 1.5 s. The preflight runs only for an OpenCode launch with a model preference and no agent arguments on OpenCode 2.0.16, twice per such launch; every other launch never pays it.
  • With a helper that ignores SIGTERM, the preflight's stop now ends 53-79 ms after the server's SIGTERM. Without the supervisor's group kill it was 3040 ms (and before this PR, that helper simply leaked).
  • Codex: a deliberate session close is unchanged in the normal case, because Codex exits on end of input. A session stuck at its deadline is now SIGTERMed and waited on up to the supervisor's stop time (5.5 s), where before it was SIGKILLed at once. A session whose Codex ignores end of input after its work is done gets end of input, 1 s, SIGTERM, 3 s, then SIGKILL, where before it got 1.5 s and then SIGKILL.

Linked Issue

Part of STA-9254 (Linear): step 2 of 2. Step 1, #25715, is merged.

Visual Proof

Nothing on screen changes; the change is which processes are still running after Orca exits. These are the live-QA logs, rendered as images. Process ids come from the rig, and liveness is read from the kernel.

Before (main a47e0f5), Orca killed during an OpenCode preflight: 8 s later, the stand-in server and its helper are still running.

main: OpenCode preflight, Orca killed

After (this PR, fba19a9), same case: the server gets SIGTERM at 0.08 s; it and its helper are gone at 0.11 s, the supervisor at 0.22 s.

PR: OpenCode preflight, Orca killed

Before (main), Orca killed during a Codex model-list session: the stand-in codex app-server sees its input end but is never signalled. 8 s later, it and its helper are still running.

main: Codex session, Orca killed

After (this PR, fba19a9), same case: input ends, SIGTERM follows 1.0 s later (the drain), the server is gone at 1.07 s, and the SIGTERM-ignoring helper is SIGKILLed after the 3 s grace. Everything is gone by 4.16 s.

PR: Codex session, Orca killed

Testing

Live QA (macOS; this PR on fba19a9, the current head, vs main a47e0f5). A rig with an isolated profile and stand-in opencode (reporting 2.0.16) and codex servers. Each stand-in ignores end of input, never answers, and starts a helper that ignores SIGTERM. The production triggers were used: an OpenCode launch with a model preference, and the Codex model-list probe. Each quit or kill landed mid-flight. Process ids were recorded and liveness was read from the kernel every 0.1 s. The PR side was first run on 2e23019 and re-run on fba19a9 after the port onto main's managed provider lifecycle; the times agree within 0.06 s.

Case main: 8 s after the event This PR: everything gone by
OpenCode preflight, quit server and helper still running, never signalled 0.26 s (SIGTERM at 0.16 s)
OpenCode preflight, crash server and helper still running 0.22 s (SIGTERM at 0.08 s)
Codex session, quit server and helper still running; input closed, never signalled 4.19 s (input end, SIGTERM 1.00 s later, helper SIGKILLed after the 3 s grace)
Codex session, crash server and helper still running 4.16 s (input end, SIGTERM 1.00 s later)

Automated (macOS). Every real-process test uses stand-in servers: Node scripts that ignore end of input, start a helper that ignores SIGTERM, and record when their input ended and when SIGTERM came. The owner tests bundle the real module into a plain-Node "Orca" with esbuild and SIGKILL it.

  • Codex sessions:
    • spawned under the supervisor in session lifetime (Windows spawns directly, as before);
    • a stuck session gets SIGTERM at its deadline, and its whole group is gone;
    • when the owner is SIGKILLed, the server sees end of input and SIGTERM only 994-1001 ms later, and then the group is gone;
    • a missing binary is reported as the spawn error and classified as missing;
    • the supervisor's internal text never appears in an error;
    • with fake timers: the tree is forced only after the full stop time, and Windows keeps its deadline kill and 1.5 s wait.
  • Codex backfill:
    • spawned under the supervisor with the Codex close request;
    • an aborted recovery stops the whole group;
    • when the owner is SIGKILLed, the server drains, then the group is gone;
    • the stop only ends input, and forces only after the full stop time.
  • OpenCode preflight:
    • the real probe reads its context from a stand-in that starts listening 1.5 s after its input ends;
    • its server group is gone when the probe returns, within 1 s of SIGTERM;
    • when the owner is SIGKILLed, the server group is gone;
    • a forced stop and a supervisor exit of 1 both return no context;
    • a relayed exit returns the context;
    • Windows keeps its existing teardown.
  • Ablations: each fix was undone and its tests failed (23 cases). Examples: a direct spawn leaves the stand-in alive after the owner dies; the session lifetime makes the preflight return nothing; the default close request gives Codex SIGTERM 20-43 ms after its input ends instead of a 1 s drain; a forced stop or an exit of 1 counted as proof returns stale context.
  • Checks run:
    • on 285b36c, after merging main: the 127-file set plus the test files that now reach these modules through main's newer code (180 files: 178 passed, 2 skipped, 0 failed);
    • the node and CLI typechecks;
    • oxlint and oxfmt on the changed files;
    • the changed-code quality gate;
    • the max-lines ratchet;
    • the reliability-gate manifest check.

Not verified:

  • The real Codex and OpenCode CLIs were not run. That opencode serve ignores end of input is from reading its source.

  • The Linux runtime and Windows CI.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

AI Disclosure

Review

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Cross-platform: macOS and Linux are supervised. Windows and WSL keep today's direct spawn and their teardown (STA-9579).
  • SSH: nothing remote changes. The preflight and these Codex servers run on the host that runs Orca.
  • Folder workspaces: unaffected. The preflight passes its working directory through unchanged, and the Codex servers run in the Codex home.
  • Mixed versions: nothing persisted and no wire change.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

…d relay a provider's last output

A one-shot CLI (claude -p, codex exec -) reads its request until stdin ends, so the
supervisor's session rule (stdin end means the owner is closing) would stop it about
1 s into its answer. The one-shot lifetime passes stdin end through and leaves only
the owner-death watch and explicit signals to stop it.

The supervisor also exited as soon as its provider did, dropping output still in the
pipes when the owner reads slowly. It now waits, bounded, for the provider's output
to be relayed before exiting.
…or on POSIX

The Claude model-list probe, model discovery for every agent, and commit message,
pull request and branch name generation all spawned the agent CLI as a plain child
of Orca. If Orca quit, crashed or was killed while one was running, nothing stopped
it, and a CLI that hung kept running after Orca was gone.

They now run under the provider supervisor that native chat already uses, in its
one-shot lifetime, so Orca's exit stops the agent's whole process group however
Orca exits. A timeout or cancel asks the supervisor to stop (SIGTERM) and only
tears the tree down once it has had its full stop time; killing the supervisor
first would orphan the agent's group. A missing binary now reaches Orca as the
supervisor's exit 127, which is mapped back to the existing not-found message.
Windows and WSL keep spawning the agent directly.
…p every spawn error back, and share one stop ladder

- The supervisor read the provider's command and arguments from one base64 JSON env string.
  Linux caps a single env string at 128 KiB, so an argv prompt of about 90-120 KiB, which
  passes the 120 KiB per-argument guard, failed execve with E2BIG. The provider argv now
  follows the supervisor script's '--' as real arguments; the env keeps only small fields.
- A supervisor that cannot start its provider reports Node's spawn error line and exits 127.
  That line now becomes the same error a direct spawn emits, so ENOENT still reads as
  'not found on PATH' and EACCES or any other spawn error reads as 'failed to start'.
- stopSupervisedProvider is the one ask, wait and force ladder: it gives a supervisor its full
  stop time before forcing. Agent one-shots, the Codex app-server close and the Claude child
  exit proof now share it, with the same requests, bounds and forced steps as before.
…derr line

Node throws most spawn failures (ENOEXEC, ENOTDIR, ELOOP, EPERM, ...) instead of emitting
them, and the supervisor had no catch, so it died with exit 1 and a stack trace that the
user saw as the agent's failure. A thrown or emitted spawn failure now exits 127 with one
marked line carrying whether it was thrown, its code and its message. Orca reads only the
last stderr line, so a runtime warning printed earlier cannot hide it, and maps it to the
message a direct spawn gave: thrown is 'could not be started', ENOENT is 'not found on
PATH', and any other emitted error is 'failed to start'.
…r and hand them to the provider

The supervisor runs Electron in Node mode, which honours NODE_OPTIONS and
NODE_REPL_EXTERNAL_MODULE. A user value such as a --require of a missing file stopped
the supervisor from starting, breaking even native CLIs like Codex that never load it.
The launch now takes both out of the supervisor's environment, carries them in its spec,
and restores them for the provider only, so a Node-based CLI still gets them.
… own breadcrumb site

The forced tree teardown recorded every self-initiated kill as the Codex app-server's, so
a forced commit-message or model-discovery stop read as a Codex teardown in crash
breadcrumbs. The teardown now takes the caller's site; source-control stops pass the
site their Windows tree kill already uses.
…tput limit; name the direct-child suites

The commit-message suites that drive fake children spawn them directly, the unsupervised
shape Windows and WSL use, so they now say so. The supervised POSIX stop gets its own
compositions: a timed-out Codex generation settles at once but holds the Codex home until
its supervisor has stopped (faithful fake, fake timers), and an agent that floods past the
output limit is stopped through its real supervisor with no process left behind.
…pervisor on POSIX

The Codex model-list probe, the hook trust grant and the session index heal each
start a short-lived `codex app-server` as a plain child of Orca, and counted on it
exiting when its input closes. A wedged Codex (a cold model/list waiting on the
network, say) kept running after Orca quit, crashed or was killed.

These sessions now run under the provider supervisor that native chat's Codex
connection already uses, in its session lifetime, so Orca's exit stops the
server's whole process group. The session's end and its deadline share the one
stop ladder: end its input (and SIGTERM a session past its deadline), give the
supervisor its full stop time, and only then tear the tree down. A missing binary
reaches Orca as the supervisor's exit 127 and is mapped back to the spawn error,
so the trust-grant telemetry still reads it as a missing binary. Windows and WSL
keep spawning the server directly, with the same timings as before.

The supervisor now takes only the command line it starts, so the CLI build, which
also runs these sessions, no longer pulls in the native chat connection's types.
Agent one-shots stop their supervisor with SIGTERM through the shared stop ladder,
watching the child's own exit. That adapter moves into one helper so the Codex
backfill recovery can use it with its own stop request, instead of a copy.
…l alive

While Codex rebuilds its session index, Orca keeps a read-only `codex app-server`
running for up to an hour so Codex can finish. It was a plain child of Orca with
its input held open, so a quit, crash or kill left it running.

On POSIX it now runs under the provider supervisor in its session lifetime, so
Orca's exit stops its whole process group. Stopping it (done, aborted, or given
up) ends its input, as a Codex connection close does; the supervisor then
SIGTERMs the group and SIGKILLs it after the grace, and the tree is torn down
only if the supervisor outlives its full stop time. Windows and WSL keep the
direct spawn and the drain-first probe termination.

The spawn and stop of that process move into their own module.
Before an OpenCode launch, Orca starts `opencode serve` to read the configured
agent and models, then stops it. The server was detached into its own process
group and never exits when its input ends, so if Orca quit, crashed or was killed
during that preflight (up to 10 s), nothing ever stopped it.

On POSIX the server now runs under the provider supervisor in its one-shot
lifetime: the preflight's closed input does not stop it, and Orca's exit does.
The preflight's teardown asks the supervisor to stop (SIGTERM) and forces the
tree only after the supervisor's full stop time; signalling or SIGKILLing the
supervisor's own group would orphan the server's. A descendant that ignores
SIGTERM is now killed with the group rather than left running once the pipes
close. Windows keeps the direct spawn and its existing teardown.
A session past its deadline is SIGTERMed through its supervisor rather than
waiting out the stdin-end grace, and its tree is torn down only after the
supervisor's full stop time; Windows keeps its deadline kill and 1.5 s close
wait. A supervised backfill app-server is stopped by ending its input, with the
same full stop time before any teardown.
…h and cover supervised discovery

The commit-message suites that drive fake children mocked the supervisor away on POSIX, so
they asserted a direct root SIGKILL that production no longer takes there. They now pin the
platform to Windows (with an empty PATH, so host installs cannot answer a bare agent name)
and assert the Windows kill, taskkill included. The three tests that check the host's own
discovery spawn shape run on the host and read the agent argv past the supervisor's '--'.
Model discovery gets its supervised composition: a timed-out Codex discovery settles at once
but holds the Codex home until its supervisor has stopped.
…e spawn error it was

Native chat's exit errors carry the provider's stderr tail into Details. Under the supervisor
a missing CLI left the supervisor's internal spawn-failure report there instead of Node's own
'spawn <cmd> ENOENT'. The report, its parser and a display formatter now live in one module;
the Codex app-server and Claude stream-json exit errors pass the tail through the formatter,
which turns a report back into the spawn error and leaves any other stderr unchanged.
…crashing on its missing pipes

When the provider spawn fails outright (EMFILE, ENFILE), Node emits 'error' later and leaves
the child with no pid and no stdio. Piping stdin into the missing pipe threw first, so the
supervisor died with exit 1 and a stack trace and never wrote its spawn-failure report. The
pipes are now wired only for a provider that started.
Agent one-shots stop their supervisor with SIGTERM through the shared stop ladder, watching
the child's own exit. That adapter moves into one helper beside the ladder, so other
supervised children can use it with their own stop request instead of a copy. The caller's
breadcrumb site still reaches the forced teardown. Same request, wait and force as before.
Takes PR1's stopSupervisedChildProcess and source-control stop as they are.
Short-lived Codex sessions now read a supervised spawn failure through PR1's
marker classifier, show their stderr through its display formatter so the
marker never reaches an error message, and file a forced teardown under the
'codex-app-server-session' breadcrumb site. The marker module joins the CLI
typecheck list, since the CLI also runs these sessions.
…er their own breadcrumb sites

A forced teardown of the Codex backfill app-server is filed under
'codex-state-db-backfill-recovery', and one of the OpenCode launch model
preflight under 'opencode-launch-model-preflight', instead of the generic
'codex-app-server-teardown'.
A loaded host let the test read the pid file between its creation and its
write (Unexpected end of JSON input); the stand-in now renames it into place.
… its owner dies

The owner-death watch went straight to the group SIGTERM and cancelled any stdin-end grace,
so when Orca quit or crashed a session provider such as the Codex app-server never saw the
EOF that lets it finish writing its state (auth.json, the state database). A session whose
owner is gone now closes as an owner's stdin end does: the provider's stdin is ended, it
gets the stdin-end grace, and only then the SIGTERM and SIGKILL ladder. A one-shot already
had its EOF at the end of its request, so its owner's death still stops it at once.
…d stop, and trust only a proven stop

The preflight's own stop wrapper waited on the supervisor's pipes and counted a
forced teardown as proof, though the teardown reports success even when it found
no descendants to check, and a supervisor that failed to reap its group exits 1
with its pipes closed. It now uses stopSupervisedChildProcess with its breadcrumb
site, and counts the server stopped only when the supervisor ended on its stop
signal or relayed the server's own exit. A forced stop, or an exit of 1, returns
no context, as an unverified stop did before.
…r exits on a stop

A requested stop waited out the whole SIGTERM grace for the provider's group even after the
provider itself had exited, so a SIGTERM-ignoring helper it left behind held every stop for
up to 3 s. Under a stop, the rest of the group is now SIGKILLed as soon as the provider has
exited, the same rule its own exit already follows.
…tput limit

Model discovery's supervised stop was covered only under timeout. A Codex discovery that
floods past the output limit now runs through a real supervisor: it settles with the
too-much-data error, its agent is stopped through the supervisor, and the next discovery on
the same Codex home starts only after that agent is gone. The direct-child suites' headers
now list exactly the supervised cases that are covered.
…er closes it

Owner death gave every session provider the stdin-end grace, so after an Orca crash a
Claude session, whose close is a stdin end plus SIGTERM, could keep working on its turn
for a second with nobody watching. The spawn spec now names the provider's close request:
'stdin-end' (the Codex app-server drains and exits on EOF, then gets its grace) or
'stdin-end-and-sigterm' (Claude; the default). A gone owner gets that same request. One
constant per provider feeds both its spawn spec and its owner-side close, through one
requestProviderClose, so the two cannot drift. One-shots still stop at once.
Keeps PR2's ProviderLaunch type beside PR1's close request in the supervisor.
…e way a Codex connection closes

Codex finishes its writes and exits on its stdin end, so the Codex connection's
supervisor closes it by ending stdin, and an owner that is gone now gets that
same close. The short-lived sessions and the backfill app-server now name the
same close request, through one shared constant, in their spawn spec and in
their own close: Orca quitting or crashing gives them the stdin end and its
grace before SIGTERM, instead of an immediate SIGTERM. A session past its
deadline still adds a SIGTERM, since it is wedged.
…nd the preflight stop no longer waits out the grace

The stand-in now records when its stdin ended and when SIGTERM arrived. A
SIGKILLed owner leaves a Codex session or backfill app-server its stdin-end grace
before SIGTERM, and the OpenCode preflight ends within 1 s of its server's SIGTERM
even with a descendant that ignores SIGTERM.
…d start on a loaded host

At 500 ms a loaded full run hit the deadline before the supervisor had started
the stub, which never wrote the pid the test reads.
…ts provider exits

Killing the rest of the group the moment the provider exited under a stop also reached
native chat's closes, so an MCP server, a tool's child or a dev server still in Claude's or
Codex's group was SIGKILLed mid-cleanup instead of getting the rest of the SIGTERM grace.
The early group kill now applies only to one-shots, where the saved wait was the point;
a session's stop is back to waiting out the grace for its group.
…itly

The spawn-spec assertion matched the default close request, so dropping Claude's explicit
request still passed. The test now checks that the spec is built with the exit-proof ladder's
own constant.
Other Codex app-server spawns will name the same close request as the connection does.
Holding it in its own small module lets them import it without the connection itself.
Main's #24989 moved the provider supervisor and its teardown into src/main/provider-process/.
PR1's changes move with them: the one-shot lifetime, the provider argv on the supervisor's argv,
the spawn-failure report and its formatter, the no-pid guard, close-request mirroring, the
one-shot-only leader-exit reap and the shared stops. Main's mechanisms replace PR1's where they
overlap: the teardown takes the caller's required site label (PR1's optional site is dropped),
and the provider's env is resolved once by resolveProviderChildEnv before the supervisor keeps
the Node options for the provider alone.
…in the CLI project

The command-line build runs the short-lived Codex app-server session, which will name the
same close request as the Codex connection. Listing the close request, the provider
supervisor it takes its type from, and the spawn-failure report the supervisor uses lets the
CLI project typecheck that import without pulling the connection in.
e65bbc9)

Ports PR2 to the provider-process layout: the supervisor, teardown, stop helper
and spawn-failure report now come from src/main/provider-process/, every forced
teardown passes its required site, the backfill's stop passes { site, request }
to stopSupervisedChildProcess, and PR2's own ProviderLaunch type gives way to
main's launch types. tsconfig.cli.json keeps PR2's teardown chain, de-duplicated.
At a load average near 50 both owner-death tests timed out waiting for the
bundled owner's stand-in at 10 s; they pass alone.
…rt, and cover a server that ignores SIGTERM

The session and trust-grant deadline tests ran a 2-4 s deadline from spawn, so a
loaded host could stop the stand-in before it wrote its pid. The session test's
deadline now outlasts its pid-read budget, and the trust-grant deadlines are 8 s.
The trust-grant comment said a wedged server may ignore everything but SIGKILL,
but that stub dies on its stdin end; a new POSIX case pins that a server ignoring
both its stdin end and SIGTERM is SIGKILLed after the SIGTERM grace.
…e reports one

On Linux, glibc's execvp hands an executable that is not a program to /bin/sh, so both a
direct and a supervised spawn run it and it exits 127; only macOS throws ENOEXEC. The
not-a-program case now runs on macOS only; the path-through-a-file case (ENOTDIR) still
covers a thrown start failure everywhere.
…odule

The WSL invocation boundary lists files that spawn wsl.exe directly. The
backfill recovery's spawn moved into codex-state-db-backfill-recovery-process.ts,
so the entry moves with it; the count is unchanged.
… stops reach the process-table reader

The backfill recovery and Codex sessions now stop through the shared supervised
teardown, whose process-table reader binds execFile when it loads. Five rate-limit
fetcher tests mocked node:child_process with only spawn and failed at load: they
now mock the backfill recovery, as their sibling fetcher tests already do. The
account add-login tests' child_process mocks gain an execFile stub.
Main's #25204 moved every structured provider's close onto one shared close
(closeProviderProcess, driven by a close policy) and spawn (spawnManagedProviderProcess).
That is the same concept as PR1's stopSupervisedProvider and requestProviderClose, so PR1's
copies are removed and its callers move onto main's:
- The Codex connection and the Claude spawn and exit proof take main's versions.
- The close request a gone owner gets is derived from the provider's close policy inside
  spawnManagedProviderProcess (signalSupervisorOnClose -> 'stdin-end-and-sigterm', else
  'stdin-end'), so one source drives both the owner's close and the supervisor's.
- stopSupervisedChildProcess (agent one-shots) runs main's closeProviderProcess with the
  root-only policy and the caller's close request.
- The Codex close-request constant and its CLI build entries go: nothing uses them now.
29e6696)

Main's shared provider lifecycle replaces PR1's stop and close-request helpers,
so PR2's callers move onto it:
- Codex sessions spawn through spawnManagedProviderProcess with the root-only
  close policy, as the Codex connection does, and close through managed.close():
  stdin end, the supervisor's full stop time, then the tree. A session past its
  deadline still gets stdin end and SIGTERM first; Windows keeps its deadline
  kill, and its forced close is now the shared Windows teardown.
- The backfill names one close request ('stdin-end') for its spawn spec and its
  stopSupervisedChildProcess stop.
- The OpenCode preflight reads the shared close result: a stop counts only when
  the supervisor reaped the group itself or a forced teardown proved the tree gone.
…gone

A forced teardown walks and group-kills the supervisor's tree, but the server
leads its own detached group, so a teardown verdict of 'exited' does not cover
members left in the server's group. Only a supervisor that reaped the group
itself, by its own stop or relaying the server's exit, now counts as a proven
stop. The Windows session-stop test now says why it sees no direct kill.
@brennanb2025

brennanb2025 commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

Status: reviewed, live-QA'd, CI green

The problem. Orca briefly runs some agent servers in the background: a Codex app-server for the model list and other short sessions, a Codex server for recovering old chats, and an opencode serve that reads model details before an OpenCode launch. When Orca quit or crashed during one of them, nothing stopped that server. A server that hung kept running after Orca was gone, along with anything it had started. A Codex server only had its input closed, and opencode serve ignores that.

What changes for you. On macOS and Linux these servers and their children now stop when Orca quits, crashes or is force-killed. Codex gets the same close as Orca's native-chat Codex connection: its input ends, SIGTERM follows 1 s later, and SIGKILL 3 s after that. Nothing on screen changes. Windows and WSL behave as before. This is step 2 of 2. Step 1 (#25715) is merged, and this PR now targets main.

Review rounds

Round Scope Verdict
1 Lifecycle One P2: on Orca quit or crash, a supervised Codex server got SIGTERM about 20-40 ms after its input ended, instead of the 1 s drain its normal close gives it. Fixed in step 1's supervisor: when Orca dies, it now closes each provider the way that provider's normal close does. Tests show SIGTERM 994-1001 ms after input ends. P3s fixed: the OpenCode preflight counted some unproven stops as proof the server was gone; stale comments.
1 Architecture against the common pattern No P0-P2. Design kept. P3s fixed: the preflight's hand-written stop now uses the shared supervised stop; after the server exits, the rest of its group is SIGKILLed at once instead of waiting out the grace (a preflight with a SIGTERM-ignoring helper went from 3040 ms to 53-79 ms). The recovery server's close is labelled as intended in the body: SIGKILL now comes at 4 s instead of 5 s, but it reaches the whole group.
Merge Port onto main's provider-process layout (#24989, #25204) One P3: a forced teardown still counted as proof the OpenCode server was gone, though it can leave members of the server's own group. Fixed at fba19a91053: only a stop that the supervisor itself completed counts.

Each fix was ablated: undoing it makes its test fail (23 ablations).

Live QA (macOS, isolated rig, stand-in servers that ignore end of input and start a SIGTERM-ignoring helper)

The PR side was re-run on fba19a91053 (then the head), after the port onto main's managed provider lifecycle (Codex sessions now start and close through main's shared provider process, and the new rule that a forced stop proves nothing). Times agree with the first run on 2e230198bba within 0.06 s. The app was driven by Codex.

Case main a47e0f56578, 8 s after the event This PR fba19a91053: everything gone by
OpenCode preflight, quit server and helper still running 0.26 s (SIGTERM at 0.16 s)
OpenCode preflight, crash server and helper still running 0.22 s (SIGTERM at 0.08 s)
Codex session, quit server and helper still running; input closed, never signalled 4.19 s (SIGTERM 1.00 s after input end)
Codex session, crash server and helper still running 4.16 s (SIGTERM 1.00 s after input end)

main: OpenCode preflight, Orca killed, server and helper still running 8 s later
this PR at fba19a91053: OpenCode preflight, Orca killed, everything gone by 0.22 s
main: Codex session, Orca killed, server and helper still running 8 s later
this PR at fba19a91053: Codex session, Orca killed, input ends, SIGTERM 1 s later, everything gone by 4.16 s

The real ~/.codex/hooks.json, ~/.codex/config.toml, ~/.claude/settings.json, ~/.claude.json and ~/.config/opencode were hashed before and after every case, in both runs. In the first run, one change to ~/.codex/config.toml was traced to the user's own running Orca, which was adding trust entries for other sessions' worktrees; no rig process touched it. In the re-run, only ~/.claude.json changed, which every running Claude session on the machine writes. Nothing else changed.

Since review: retarget and merges with main

  • Retargeted to main after fix: stop Claude and agent helper processes when Orca quits or crashes (STA-9254, 1 of 2) #25715 merged. fix: stop Claude and agent helper processes when Orca quits or crashes (STA-9254, 1 of 2) #25715 is on main as squash 19661dfca67; its files are identical to main here, so this PR's diff is only its own 21 files.
  • Merged main three times (a merge, never a rebase), most recently c893048aebc. Two conflicts:
    • config/tsconfig.cli.json: kept main's new entries plus this PR's, and dropped two entries for files main deleted.
    • codex-hook-trust-grant.ts: took main's file. Main removed the timeout this PR's comment described, so that comment change is gone and the file is no longer in this PR.
  • The live QA ran at fba19a91053. Since then this PR's own code has not changed, apart from a doc comment main reworded in codex-app-server-session.ts. The shared provider-process code it uses changed only in two ways: Windows taskkill now reports its result, and there is a new opt-in close option that these providers don't set. Neither touches the macOS paths the QA exercised.

CI

Head 285b36c21a9: 14 passed, 16 skipped, 0 failed, including all five test shards. Two earlier heads went red for reasons outside this PR:

Not verified

  • The real Codex and OpenCode CLIs. Every test and the live QA used stand-ins. That opencode serve ignores end of input is from reading its source.
  • The Linux runtime, and Windows beyond CI.
  • Windows: the port's Windows stop (taskkill) is covered only by unit tests; it can't be exercised on macOS.

@brennanb2025
brennanb2025 changed the base branch from brennanb2025/supervised-one-shot-cli to main October 6, 2026 19:17
…de-probes

#25715 (step 1) landed on main as 19661df; this brings PR 2 onto main.
…de-probes

codex-hook-trust-grant.ts takes main's side: #25743/#25744 removed the background
trust-grant timeout whose comment PR 2 had updated, so that comment change is
dropped. tsconfig.cli.json keeps main's edits plus PR 2's teardown-chain entries,
without the two real-home grant files main deleted.
…de-probes

Brings in #25977, main's fix for its own typecheck error, so CI's test shards run.
@brennanb2025
brennanb2025 marked this pull request as ready for review October 7, 2026 00:04
@coderabbitai

coderabbitai Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: b5f2a918-dfa1-4539-8f72-bdc74e0a89b4
📥 Commits

Reviewing files that changed from the base of the PR and between c893048 and 285b36c.

📒 Files selected for processing (21)
  • config/tsconfig.cli.json
  • src/main/codex-accounts/service-account-add-login.test.ts
  • src/main/codex/codex-app-server-client.test.ts
  • src/main/codex/codex-app-server-session-stop.test.ts
  • src/main/codex/codex-app-server-session-supervision.test.ts
  • src/main/codex/codex-app-server-session.ts
  • src/main/codex/codex-model-catalog-probe.ts
  • src/main/codex/codex-state-db-backfill-recovery-process.test.ts
  • src/main/codex/codex-state-db-backfill-recovery-process.ts
  • src/main/codex/codex-state-db-backfill-recovery-supervision.test.ts
  • src/main/codex/codex-state-db-backfill-recovery.test.ts
  • src/main/codex/codex-state-db-backfill-recovery.ts
  • src/main/codex/supervised-probe-owner.test-fixture.ts
  • src/main/opencode/opencode-launch-model-context-cleanup.test.ts
  • src/main/opencode/opencode-launch-model-context-supervision.test.ts
  • src/main/opencode/opencode-launch-model-context.ts
  • src/main/rate-limits/codex-fetcher-auth-errors.test.ts
  • src/main/rate-limits/codex-fetcher-backend.test.ts
  • src/main/rate-limits/codex-fetcher-credit-balance.test.ts
  • src/main/rate-limits/codex-fetcher-probe-shutdown.test.ts
  • src/main/rate-limits/codex-fetcher-session-supplement.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

Codex app-server sessions and state-DB backfill recovery now use managed or supervised process lifecycle helpers. OpenCode launch-context probes create one-shot provider spawn specifications and use supervised cleanup when applicable. Tests cover platform-specific spawning, timeout behavior, process-tree shutdown, and process-owner termination. The CLI TypeScript project includes additional process-management modules, and several rate-limit and account tests update their child-process mocks.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 285b3

No actionable issue remains from this review; the PR is mergeable after normal checks.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 30 functions across 20 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: stopping Codex and OpenCode helper servers when Orca exits or crashes. The issue and step suffix add some length but do not obscure the change.
Description check ✅ Passed The description is detailed and mostly complete. It covers the user-facing change, rationale, implementation, linked issue identifier, visual evidence, testing, platform limitations, and checklist. Th…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 26.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 30 functions across 20 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@brennanb2025
brennanb2025 merged commit 61bcca9 into main Oct 7, 2026
54 checks passed
brennanb2025 added a commit that referenced this pull request Oct 7, 2026
…ss now imports

Main's #25753 added managed-provider-process.ts to the CLI project, which
imports this PR's provider-executable-missing.ts.
brennanb2025 added a commit that referenced this pull request Oct 8, 2026
…d, without disabling Send (#25666)

* fix(native-chat): disable Send for unavailable agents

* test: update sign-in copy expectations

* fix: show automatic chat availability only on Send

* fix: keep signed-out evidence when a live session lists models

* refactor: split Send availability presentation and tests

* refactor(native-chat): keep the sign-in verdict on the catalog's failure record

The probe's "signed out" / "CLI missing" verdict now lives on the account's
existing model-catalog failure record instead of two extra maps, with the
failure TTL as its only lifetime:

- Only the probe sets or clears the verdict; a chat's own model listing
  (which works while signed out) never clears or renews it. An aged-out
  verdict makes the next read re-probe, however fresh the catalog.
- A healthy fresh catalog is no longer re-probed every 30 s; it keeps the
  normal 10 min fresh window. A Claude start refused as signed out ages the
  catalog so the next read re-probes.
- A waiting picker again answers from whichever catalog lands first and never
  waits on a probe. A read that still owes the probe's verdict says so, and the
  composer waits for it once with the new optional waitForAvailability param.
- The open-time start-failure line and its transcript row are hidden only
  while Send is disabled for the same reason; otherwise they show as before.
- A host verdict lifetime longer than the client's hold is clamped, not
  dropped. The catalog read watches only the account settings and the paired
  host's contact instead of every store write.

* fix(native-chat): one send-gate rule, and a missing CLI as its own failure

- The sign-in/CLI verdict is gated once where the session hands it to the
  composer: while a turn runs it neither shows nor refuses, so a follow-up
  queues as before; goal mode is held by the same gate. The start-failure line
  and row hide only while that same value is up.
- A start whose CLI was never found (a direct spawn's ENOENT, or the one a
  provider supervisor reports) is now a `cliMissing` failure with the disabled
  Send's sentence, instead of a generic "couldn't start" beside it. An older
  client shows the host's sentence for the new kind.
- Signed-out words for a slash command keep "Run /x again".
- The Claude catalog reads its models only from the reply to its own
  list_models request, never from the initialize reply.
- The Codex probe lists models while its account check runs; a signed-out
  verdict still wins.
- The missing-executable check moves to provider-process, out of native chat.

* test(native-chat): signed-out command failures name the command to run again

* fix(native-chat): probe the Codex home a launch would read

- Before the catalog probe's read-only app-server reads Orca's shared Codex
  home, it runs the same sync a launch and the usage poll run, so a
  `codex login` made in ~/.codex is seen at once instead of leaving
  "Run codex login" up until something else re-syncs (forever on a headless
  host). Per-account homes are left alone.
- A "no account" answer counts as signed out only when the home's auth.json is
  absent or holds no credential; one caught mid-write by a token refresh, or
  holding a credential, is unknown.

* test(codex-accounts): type the node:os mock with a type import

* fix(native-chat): keep the sign-in verdict until a newer answer replaces it

- No gap while signed out: the host serves an aged verdict while the probe that
  re-derives it runs, and the composer never clears its last answer before a
  read; only an answer without a verdict, an error, lost contact or an account
  change does. Send stays disabled with its reason through every re-check.
- After sign-in the stale "isn't signed in" start-failure line stays hidden:
  a probe that found the account fine now says so (`accountVerified`), and the
  line shows only when the host has no answer at all.
- A Claude start refused as signed out, or any start whose CLI was never
  found, is recorded as that home's verdict, and the chat reads it once when
  that start failure arrives.
- The re-probe mark is read only where the catalog read starts its probe, so a
  Codex chat's own picker no longer re-lists for it on every read.
- Choosing an account or signing in again in Accounts settings re-derives the
  verdict at the next read; the account key now compares settings by value.
- A re-read that brings an identical catalog keeps the option state, so focus
  re-reads re-render nothing.

* fix(native-chat): Codex's signed-out words name only the step that unblocks this chat

A chat is pinned to the Codex home it started in, so choosing another account
in Codex Accounts settings never unblocks it. The system-account sentence now
says only to run `codex login`, in every locale.

* refactor(native-chat): give an account's sign-in status its own owner

The sign-in verdict lived inside the model catalog's failure records, so
the store needed guards for who wrote a failure, which listing may clear
it, and whether a verdict was served while re-probing. Each guard fixed one
way a chat's own listing or an older probe overwrote the account's answer.

The answer now lives in AgentAccountStatuses: one in-memory, capped status
per account home (ready, not signed in, or CLI missing). Only the
session-less probe, a refused start and an account change write it. A
blocked answer older than 30 s, or one an account change marked, makes the
next read start the probe whatever else is listing, and it is served until
that probe answers. Older evidence never replaces newer. The catalog's
failure code is main's again, and agent-model-catalog-failures.ts is gone.

The wire carries one optional `availability` field in place of
`unavailable` and `accountVerified`. The renderer holds its answer per
host, agent and chat, re-reads only on that agent's account inputs, and
re-reads on a timer only while blocked. After an account change, Send
stays disabled with the previous words until the re-probe answers, then
changes once. Account switches and re-sign-ins mark the status before the
settings change a chat's read follows.

* fix(native-chat): re-derive the sign-in answer after an account change mid-probe, and back off while blocked

Round-4 review findings on the account status owner:

- An account switch or re-sign-in made while a probe was running left the
  old answer up for up to 30 s: the next read joined that probe, whose
  answer was then dropped as older than the change. A read now chains
  exactly one fresh probe after a probe that began before the change.
- Removing the selected Claude account swapped another sign-in into the
  shared config dir without marking the answer; it now marks it first.
- Evidence was ordered by wall-clock time, so a clock that stepped back
  froze a blocked answer. It is now ordered by a sequence and aged on a
  monotonic clock; a negative age reads as due.
- A chat left open while signed out started a CLI probe every 30 s.
  Probes that keep finding the same block now space out (30 s, 60 s,
  2 min, 4 min, then every 5 min), carried to the client as recheckInMs.
  An account change or a different refusal starts the hold over.
- The renderer dropped no settled wait from its leave set; it now does.
- A failed Claude probe logged its whole stdout, which now includes the
  initialize reply naming the account. Control replies are dropped from
  the log and the failure message.
- The account services take the status re-check as an injected
  onSignInChanged hook instead of importing the native-chat store.
- Imports moved below file headers; comments no longer say "ready"
  means re-checked after the failure.

* test(claude-accounts): say why the runtime-auth fakes are cast where the hook was added

* fix(native-chat): type the held-answer handle as the account status

* fix(native-chat): re-check a block when someone returns, and catch an account change during a home's first probe

Round-5 review findings:

- After a chat sat blocked for a few minutes, the backoff made every
  read wait out a hold of up to 5 minutes, so someone who ran `/login`
  or `codex login` and came back still saw Send disabled for minutes.
  The backoff now applies only to the client's own timer read, which
  it marks with an optional `scheduledRecheck` param. A read from
  someone looking (window focus, the pane becoming visible, a new chat,
  a start failure) re-probes once the block is older than 30 s. The
  param is sent only on that timer, which only a blocked `availability`
  answer arms, so a host that predates the param never receives it.
- An account change made during a home's first probe marked nothing,
  because only existing statuses were marked, so the old account's
  answer was written as current. Each agent's last account change is
  now one sequence number that every status and running probe is
  compared against; statuses are no longer mutated in place.
- Removing an account that nothing had selected no longer re-checks.

* refactor(native-chat): keep the sign-in and missing-CLI words in their own module

Main's retry wording pushed the failure-words table past its line budget.

* refactor(native-chat): build the structured chat's view session in the view-state module

Main's send-reveal work brought NativeChatStructuredSession.tsx and the
session controller to their line budgets.

* fix(native-chat): keep a stated start line hidden through a running turn, and re-probe an account changed mid first probe

- A chat whose open-time start failed signed out had that line hidden
  while the host said the same. When a new signed-out answer landed
  during a running turn, the line came back for the whole turn: the
  hide read Send's gate, which a running turn suspends. The hide now
  reads the host's answer itself (blocked for the same reason, or
  ready); Send's gate still steps aside while a turn runs. Both are
  views of the one answer the session holds.
- An account change during a home's first probe dropped that probe's
  answer but left nothing to re-check the new account, and the picker's
  reply said no probe was pending, so the chat stayed unknown until the
  catalog aged out. A read now chains one fresh probe whenever the
  running probe began before the change, and its reply says that probe
  is still owed so the client asks once more for its verdict.
- The client's timer read no longer carries `scheduledRecheck`. The
  timer already fires only when the host's backed-off `recheckInMs`
  says, which is past the 30 s after which any read re-probes, and
  every answer re-arms it. One fewer strict-schema param.

* build(cli): typecheck the missing-executable check the provider process now imports

Main's #25753 added managed-provider-process.ts to the CLI project, which
imports this PR's provider-executable-missing.ts.

* fix(native-chat): disable Send with the fix when Claude or Codex is signed out or missing

What you see: in a native Claude or Codex chat whose CLI is signed out, or not installed on the
computer running the chat, Send is disabled and its tooltip (hover or keyboard focus) says how to
fix it, in words that match the account ("Codex isn't signed in. Run `codex login`.", "This
Claude account isn't signed in. Sign in again in Claude Accounts settings.", "Claude isn't
installed on the computer running this chat."). A message refused after sending gets the same
words. On desktop the open-time "Chat could not be started" line, its Retry and its transcript row
no longer repeat these two reasons, unless a message of yours is still waiting on that start.
An unknown answer never disables Send, and while a turn runs Send stays Stop and Enter queues.

How: the host's existing session-less model-catalog probe also says why no chat can start.
Claude's probe child sends initialize before list_models and applies the rule chat start already
uses (token source none with no API key); Codex's probe asks account/read beside model/list; a
spawn that finds no executable is "CLI missing". The probe's failure record in the catalog store
carries that reason, and every catalog reply carries it as an optional `unavailable` field. Only
a probe writes or clears it: a live chat's own listing (which a signed-out Codex still answers)
never does. It lasts the store's 30 s failure lifetime; after that the next read starts or joins
the probe, serving the held reason meanwhile, and only a read that already asks to wait gets the
probe's answer. A change to the managed-account settings expires it. Before probing the shared
Codex mirror home, the probe runs the same sync a launch does. The renderer keeps the latest
answer until the next one replaces it, and only while blocked re-reads on window focus and when
the chat gains focus. No status store, timers or new request params.

* fix(native-chat): no picker loading while Send re-checks a sign-in, and plain keyboard focus on the disabled Send

A chat whose account is signed out and has no model list no longer shows the model picker as
loading each time it re-checks the sign-in after 30 s: the re-check is for Send, so a reply that
already carries the verdict ends the picker's wait. The disabled Send's wrapper takes keyboard
focus (so its tooltip opens) without becoming a second button; the inner button keeps its label.
Trims tests that repeated rules other tests already prove.

* fix(native-chat): lift the Send gate whenever Stop shows, including while a queued card is about to send

The composer shows Stop while the host's queue is about to send its next card, but the sign-in
gate stayed on in that window, so Enter was refused with no message.

* fix(native-chat): never disable Send for a sign-in or missing-CLI verdict; show a dismissible notice instead

The host's pre-send check can be wrong while the chat would work (saved
Arguments it never sees, project settings it can't read, gateways that
accept a signed-out Codex), and a disabled Send left no way past it.

- Delete the Send gate: the composer, Enter and goal mode no longer read
  the verdict, and the Send tooltip reason and focusable wrapper go back
  to main.
- Show Codex not-signed-in and Claude/Codex not-installed as one entry in
  the chat's existing notice card. Dismissed per verdict; shows again
  when the verdict changes or clears and returns; steps aside when the
  failed start (launch line or latest failed-start row) says the same.
- Claude: drop the probe's signed-out verdict (initialize-on-probe stdin,
  request-id filtering, control-reply log scrubbing). The chat start's
  refusal, with its account kind, is the only Claude sign-in signal.
  Missing-CLI from the probe stays.
- Restore the failed-start line and transcript row for these causes
  (no more hiding on desktop) and drop the chat-root focus re-check; a
  window-focus re-read stays while a notice is showing.

* fix(native-chat): re-check the notice's reason after a chat starts, keep a signed-out Codex's model list

Review round 1 of the notice rework.

- A chat that proves its start (at acquire, or on its later "started"
  event) marks the catalog's held reason for that account due, so the
  next read re-probes instead of trusting it until the TTL; the chat
  re-reads when a turn starts or ends while a notice shows.
- The Codex probe keeps a model list it got beside a signed-out reason,
  so a gateway user with a wrong verdict keeps the picker list.
- Failure records move to their own module; an expired failure with no
  reason is dropped, and expiry drops reason-less failures.
- Notice: any verdict change resets the dismissal; signed-out is
  Codex-only; the say-once rule ignores a failed start a turn has run
  past.
- Missing-CLI copy: "{{agent}} wasn't found on the computer running this
  chat. Install it, or check its Command in Settings → Agents." (6
  locales).

* fix(native-chat): only an acquiring start re-checks the catalog reason; drop the unreachable Claude filter

Review round 2 of the notice rework.

- A re-attach to a live child proved nothing new, so only an attach that
  acquired a child which proved its start marks the reason due.
- Reword: a start proves the program exists; for a Codex sign-in it only
  prompts an earlier re-check.
- Drop the notice's Claude signed-out filter: no host sends that verdict.
- Tests: a re-attach does not notify; a turn ending re-reads once more.

* fix(native-chat): never newly show a catalog reason the host is still re-checking

After signing in to Codex, a new chat flashed the old "isn't signed in"
notice for about half a second: past its 30 s lifetime the host answers a
plain read with the held reason while its probe re-checks. A chat now
adopts such a reason only when it already shows it; otherwise the joined
read's answer decides.

* test(native-chat): follow main's harness and the clearer Claude sign-in words after the merge

* test(native-chat): run the start-proof catalog test in the Node runtime project with the other SQLite host tests
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.

1 participant