Skip to content

Add startup Codex provider health checks and surface status in server config/chat banner + Windows BS - #163

Merged
juliusmarminge merged 8 commits into
mainfrom
t3code/codex-health-error-state
Mar 4, 2026
Merged

juliusmarminge merged 8 commits into
mainfrom
t3code/codex-health-error-state

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Mar 4, 2026 •

Copy link
Copy Markdown
Member

Summary

  • add a new ProviderHealth service/layer that runs startup-time Codex CLI health probes (codex --version and codex auth status --json) and caches provider status
  • include provider status data in server config contracts (ServerProviderStatus) and propagate it through server.getConfig and serverConfigUpdated
  • wire ProviderHealthLive into server startup so status is available to WebSocket handlers
  • update web chat UI to show a provider health warning/error banner for the active provider when status is not ready
  • extend server and web tests to validate provider status propagation and startup health parsing behavior

Testing

  • apps/server/src/provider/Layers/ProviderHealth.test.ts: covers ready, missing CLI, unauthenticated, and unsupported auth command cases
  • apps/server/src/wsServer.test.ts: verifies providers is included in server.getConfig and serverConfigUpdated payloads
  • apps/web/src/wsNativeApi.test.ts: verifies providers is handled and forwarded with server config updates
  • Not run: bun lint
  • Not run: bun typecheck

Note

Medium Risk
Expands the ServerConfig contract and WebSocket payloads to include provider status, which can break older clients, and changes process spawning/termination behavior (notably on Windows). The health probing runs at startup and shells out to codex, so failures/timeouts could impact perceived readiness and logging.

Overview
Adds a new startup ProviderHealth layer that probes the Codex CLI (codex --version + codex login status), parses auth/readiness, and caches a provider status snapshot.

Extends contracts so server.getConfig and server.configUpdated now include providers: ServerProviderStatuses, wires this into wsServer, and updates the web ChatView to display a warning/error banner when the active provider is not ready.

Improves Windows compatibility for spawned processes by using shell: true where needed and killing full process trees via taskkill (Codex app-server manager, text generation, detached editor launch, generic runProcess), adjusts PTY adapter selection on Windows, and tweaks dev-runner/dev script behavior; adds/updates tests covering provider health and config propagation.

Written by Cursor Bugbot for commit a2c8f59. This will update automatically on new commits. Configure here.

Note

Add startup Codex provider health checks and display provider status in server config and chat banner while fixing Windows process spawning and termination across server utilities

Introduce a ProviderHealth service that computes Codex CLI availability/auth on startup and returns statuses, expose statuses in server config and push updates, and render a provider health banner in chat; standardize Windows process spawning with shell: process.platform === "win32" and kill full process trees via taskkill, with clearer Windows "command not found" errors. Key entry points: ProviderHealth.ts, wsServer.ts, and ChatView.tsx.

📍Where to Start

Start with the provider status computation in ProviderHealthLive and checkCodexProviderStatus in apps/server/src/provider/Layers/ProviderHealth.ts, then follow how results flow into server responses in apps/server/src/wsServer.ts and UI rendering in apps/web/src/components/ChatView.tsx.

Macroscope summarized a2c8f59.

- add startup ProviderHealth service/layer to probe Codex install and auth state
- extend server contracts and WS payloads to include provider statuses
- show non-ready provider warnings in ChatView and cover with server/web tests
@coderabbitai

coderabbitai Bot commented Mar 4, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e7da37ac-457e-4c1a-8f5c-d28aa610e4b1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch t3code/codex-health-error-state

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

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Exported function makeProviderHealthLive is never used
    • Removed the unused makeProviderHealthLive function, which was confirmed to have no consumers anywhere in the codebase.

Create PR

Or push these changes by commenting:

@cursor push 4b7e2957f3
Preview (4b7e2957f3)
diff --git a/apps/server/src/provider/Layers/ProviderHealth.ts b/apps/server/src/provider/Layers/ProviderHealth.ts
--- a/apps/server/src/provider/Layers/ProviderHealth.ts
+++ b/apps/server/src/provider/Layers/ProviderHealth.ts
@@ -240,7 +240,3 @@
   });
 
 export const ProviderHealthLive = Layer.effect(ProviderHealth, makeProviderHealth());
-
-export function makeProviderHealthLive(run?: CommandRunner) {
-  return Layer.effect(ProviderHealth, makeProviderHealth(run));
-}

- Parse nested/array JSON auth status output from `codex auth status --json`
- Return `unauthenticated` when JSON contains `authenticated: false`
- Return warning when JSON is parseable but lacks an auth marker
@juliusmarminge juliusmarminge changed the title Add startup Codex provider health checks and surface status in server config/chat banner Add startup Codex provider health checks and surface status in server config/chat banner + Windows BS Mar 4, 2026

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Windows turbo command regression removes platform handling
    • Added shell: process.platform === "win32" to the ChildProcess.make options for the turbo spawn call, consistent with all other spawn calls in the PR.

Create PR

Or push these changes by commenting:

@cursor push 54822a3a0c
Preview (54822a3a0c)
diff --git a/scripts/dev-runner.ts b/scripts/dev-runner.ts
--- a/scripts/dev-runner.ts
+++ b/scripts/dev-runner.ts
@@ -415,6 +415,7 @@
         stderr: "inherit",
         env,
         extendEnv: false,
+        shell: process.platform === "win32",
       },
     );

Comment thread scripts/dev-runner.ts Resolved
Comment on lines +44 to +54
function detailFromResult(result: ProcessRunResult): string | undefined {
if (result.timedOut) return "Timed out while running command.";
const stderr = nonEmptyMessage(result.stderr);
if (stderr) return stderr;
const stdout = nonEmptyMessage(result.stdout);
if (stdout) return stdout;
if (result.code !== null && result.code !== 0) {
return `Command exited with code ${result.code}.`;
}
return undefined;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low Layers/ProviderHealth.ts:44

When a process is terminated by a signal (e.g., SIGSEGV, SIGABRT), result.code is null and result.signal is populated. The check at line 50 skips error reporting when code is null, so detailFromResult returns undefined for signal-terminated processes. This causes checkCodexProviderStatus to display a generic "failed to run" message instead of indicating the process crashed.

 function detailFromResult(result: ProcessRunResult): string | undefined {
   if (result.timedOut) return "Timed out while running command.";
+  if (result.signal !== null) {
+    return `Process terminated by signal ${result.signal}.`;
+  }
   const stderr = nonEmptyMessage(result.stderr);
   if (stderr) return stderr;
   const stdout = nonEmptyMessage(result.stdout);
   if (stdout) return stdout;
   if (result.code !== null && result.code !== 0) {
     return `Command exited with code ${result.code}.`;
   }
   return undefined;
 }
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/Layers/ProviderHealth.ts around lines 44-54:

When a process is terminated by a signal (e.g., SIGSEGV, SIGABRT), `result.code` is `null` and `result.signal` is populated. The check at line 50 skips error reporting when `code` is `null`, so `detailFromResult` returns `undefined` for signal-terminated processes. This causes `checkCodexProviderStatus` to display a generic "failed to run" message instead of indicating the process crashed.

Evidence trail:
apps/server/src/provider/Layers/ProviderHealth.ts lines 44-54 (detailFromResult function with the check at line 50: `if (result.code !== null && result.code !== 0)`), apps/server/src/processRunner.ts lines 13-21 (ProcessRunResult interface showing both `code: number | null` and `signal: NodeJS.Signals | null`), apps/server/src/provider/Layers/ProviderHealth.ts lines 198-208 (checkCodexProviderStatus showing how undefined detail leads to generic message)

Comment thread apps/server/src/processRunner.ts Resolved
Comment thread scripts/dev-runner.ts Resolved

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Duplicated Windows process kill helper across two files
    • Removed the duplicate killChildTree from codexAppServerManager.ts and replaced it with an import of the now-exported killChild from processRunner.ts, consolidating the Windows taskkill /T logic in a single location.

Create PR

Or push these changes by commenting:

@cursor push 17aee2c430
Preview (17aee2c430)
diff --git a/apps/server/src/codexAppServerManager.ts b/apps/server/src/codexAppServerManager.ts
--- a/apps/server/src/codexAppServerManager.ts
+++ b/apps/server/src/codexAppServerManager.ts
@@ -1,8 +1,10 @@
-import { type ChildProcessWithoutNullStreams, spawn, spawnSync } from "node:child_process";
+import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
 import { randomUUID } from "node:crypto";
 import { EventEmitter } from "node:events";
 import readline from "node:readline";
 
+import { killChild } from "./processRunner.js";
+
 import {
   ApprovalRequestId,
   EventId,
@@ -104,23 +106,6 @@
   "does not exist",
 ];
 
-/**
- * On Windows with `shell: true`, `child.kill()` only terminates the `cmd.exe`
- * wrapper, leaving the actual command running. Use `taskkill /T` to kill the
- * entire process tree instead.
- */
-function killChildTree(child: ChildProcessWithoutNullStreams): void {
-  if (process.platform === "win32" && child.pid !== undefined) {
-    try {
-      spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
-      return;
-    } catch {
-      // fallback to direct kill
-    }
-  }
-  child.kill();
-}
-
 export function normalizeCodexModelSlug(
   model: string | undefined | null,
   preferredId?: string,
@@ -493,7 +478,7 @@
     context.output.close();
 
     if (!context.child.killed) {
-      killChildTree(context.child);
+      killChild(context.child);
     }
 
     this.updateSession(context, {

diff --git a/apps/server/src/processRunner.ts b/apps/server/src/processRunner.ts
--- a/apps/server/src/processRunner.ts
+++ b/apps/server/src/processRunner.ts
@@ -85,7 +85,7 @@
  * wrapper, leaving the actual command running. Use `taskkill /T` to kill the
  * entire process tree instead.
  */
-function killChild(child: ChildProcessHandle, signal: NodeJS.Signals = "SIGTERM"): void {
+export function killChild(child: ChildProcessHandle, signal: NodeJS.Signals = "SIGTERM"): void {
   if (process.platform === "win32" && child.pid !== undefined) {
     try {
       spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });

}
}
child.kill();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated Windows process kill helper across two files

Low Severity

killChildTree in codexAppServerManager.ts and killChild in processRunner.ts are near-identical implementations of the same Windows taskkill /T process-tree kill logic. The only difference is that killChild accepts an optional signal parameter. This duplication means a future bug fix in one copy could easily be missed in the other.

Additional Locations (1)

Fix in Cursor Fix in Web

Comment on lines +112 to +122
function killChildTree(child: ChildProcessWithoutNullStreams): void {
if (process.platform === "win32" && child.pid !== undefined) {
try {
spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
return;
} catch {
// fallback to direct kill
}
}
child.kill();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium src/codexAppServerManager.ts:112

When taskkill fails, spawnSync returns an error object instead of throwing, so the function returns early and never falls back to child.kill(). This leaves the codex process running on Windows when taskkill fails or is not found. Check result.error or result.status after the spawnSync call and only return early on success, otherwise fall through to child.kill().

function killChildTree(child: ChildProcessWithoutNullStreams): void {
   if (process.platform === "win32" && child.pid !== undefined) {
-    try {
-      spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
-      return;
-    } catch {
-      // fallback to direct kill
-    }
+    const result = spawnSync("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
+    if (result.error === undefined && result.status === 0) {
+      return;
+    }
   }
   child.kill();
 }
Also found in 1 other location(s)

apps/server/src/processRunner.ts:88

spawnSync does not throw an exception when the process fails to spawn (e.g., ENOENT) or exits with a non-zero code; instead, it returns an object containing error or status properties. The try...catch block around spawnSync is therefore ineffective at detecting failures. If taskkill fails to execute (for example, due to permission issues or missing executable), the code hits the return; statement and exits the function early. This causes the fallback child.kill(signal) to be skipped, potentially leaving the process running. You should check the return value of spawnSync (e.g., result.error or result.status) before returning.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/codexAppServerManager.ts around lines 112-122:

When `taskkill` fails, `spawnSync` returns an error object instead of throwing, so the function returns early and never falls back to `child.kill()`. This leaves the codex process running on Windows when taskkill fails or is not found. Check `result.error` or `result.status` after the `spawnSync` call and only return early on success, otherwise fall through to `child.kill()`.

Evidence trail:
apps/server/src/codexAppServerManager.ts lines 112-120 at REVIEWED_COMMIT show that `spawnSync` is called followed by unconditional `return;` inside a try block, with catch intended to fall through to `child.kill()`. Node.js documentation (https://nodejs.org/api/child_process.html#child_processspawnsynccommand-args-options) confirms that `spawnSync` returns an object with `error` and `status` properties rather than throwing on command failure.

Also found in 1 other location(s):
- apps/server/src/processRunner.ts:88 -- `spawnSync` does not throw an exception when the process fails to spawn (e.g., `ENOENT`) or exits with a non-zero code; instead, it returns an object containing `error` or `status` properties. The `try...catch` block around `spawnSync` is therefore ineffective at detecting failures. If `taskkill` fails to execute (for example, due to permission issues or missing executable), the code hits the `return;` statement and exits the function early. This causes the fallback `child.kill(signal)` to be skipped, potentially leaving the process running. You should check the return value of `spawnSync` (e.g., `result.error` or `result.status`) before returning.

Comment thread apps/server/src/open.ts
Comment on lines 226 to +238
export const launchDetached = (launch: EditorLaunch) =>
Effect.callback<void, OpenError>((resume) => {
let child;
try {
child = spawn(launch.command, [...launch.args], {
detached: true,
stdio: "ignore",
});
} catch (error) {
return resume(
Effect.fail(new OpenError({ message: "failed to spawn detached process", cause: error })),
);
Effect.gen(function* () {
if (!isCommandAvailable(launch.command)) {
return yield* new OpenError({ message: `Editor command not found: ${launch.command}` });
}

const handleSpawn = () => {
child.unref();
resume(Effect.void);
};
yield* Effect.callback<void, OpenError>((resume) => {
let child;
try {
child = spawn(launch.command, [...launch.args], {
detached: true,
stdio: "ignore",
shell: process.platform === "win32",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Critical src/open.ts:226

When shell: true is set on Windows, launch.args containing user-provided cwd values are passed through the shell without guaranteed sanitization, allowing command injection via shell metacharacters in the path. Remove shell: true and resolve the .cmd extension manually before spawning, or use cross-spawn to handle Windows executable resolution safely.

-        shell: process.platform === "win32",
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/open.ts around lines 226-238:

When `shell: true` is set on Windows, `launch.args` containing user-provided `cwd` values are passed through the shell without guaranteed sanitization, allowing command injection via shell metacharacters in the path. Remove `shell: true` and resolve the `.cmd` extension manually before spawning, or use `cross-spawn` to handle Windows executable resolution safely.

Evidence trail:
apps/server/src/open.ts lines 235-239: `spawn(launch.command, [...launch.args], { ... shell: process.platform === "win32" })`
apps/server/src/open.ts lines 205-224: `resolveEditorLaunch` creates args containing `input.cwd`
packages/contracts/src/editor.ts lines 14-17: `OpenInEditorInput` schema with `cwd: TrimmedNonEmptyString`
packages/contracts/src/baseSchemas.ts line 4: `TrimmedNonEmptyString = TrimmedString.check(Schema.isNonEmpty())` - no shell metacharacter sanitization
packages/contracts/src/ws.ts line 106: `tagRequestBody(WS_METHODS.shellOpenInEditor, OpenInEditorInput)` - confirms cwd is user-controlled via WebSocket

@juliusmarminge
juliusmarminge merged commit 2ae63f4 into main Mar 4, 2026
5 checks passed
@juliusmarminge
juliusmarminge deleted the t3code/codex-health-error-state branch March 4, 2026 05:03
…g/t3code into t3code/codex-health-error-state
- Run Codex-related child processes with `shell` on Windows for reliable command execution
- Switch provider auth probe to `codex login status` and update health-check expectations
- Use Node PTY on Windows instead of Bun PTY in server runtime setup
…d ENOENT)

Add tree-kill via taskkill /T /F in processRunner and codexAppServerManager so timeout enforcement and session stop properly terminate the entire process tree instead of just the cmd.exe wrapper. Detect Windows command not found (exit 9009) in normalizeExitError. Pre-validate editor commands with isCommandAvailable in open.ts before spawning. Fix ProviderHealth test import and dev-runner test path assertion for Windows.

Made-with: Cursor
TomCohenDev pushed a commit to TomCohenDev/t3code that referenced this pull request Oct 18, 2026
…bhook-retry

fix(fork): repair webhook retries for 15 minutes and dedupes only delivered calls
TomCohenDev pushed a commit to TomCohenDev/t3code that referenced this pull request Oct 18, 2026
The repair webhook runs as the "Fork repair" task on the VPS. The docs said
only "a T3 server". Also note that each VPS auto-update restarts its server
for about 20 seconds, which the 15-minute retry of pingdotgg#163 covers.
HaukeSchnau pushed a commit to HaukeSchnau/t3code that referenced this pull request Nov 13, 2026
…entities

Answers CodeRabbit on pingdotgg#163 and pingdotgg#164: each sign-in gets its own binding cookie,
a full pending map turns new starts away instead of pushing out live ones, the
token exchange times out including its body, and logs name a fixed stage
instead of provider data, subjects or emails. The app sends a random
client_state that must come back with the result, and a sheet that cannot open
fails instead of throwing. A managed policy without remote clients stops the
server from offering sign-in. The add-environment dialog ignores a lookup for a
host the field no longer shows, and the route tests run under it.effect.
HaukeSchnau pushed a commit to HaukeSchnau/t3code that referenced this pull request Nov 13, 2026
…entities

Answers CodeRabbit on pingdotgg#163 and pingdotgg#164: each sign-in gets its own binding cookie,
a full pending map turns new starts away instead of pushing out live ones, the
token exchange times out including its body, and logs name a fixed stage
instead of provider data, subjects or emails. The app sends a random
client_state that must come back with the result, and a sheet that cannot open
fails instead of throwing. A managed policy without remote clients stops the
server from offering sign-in. The add-environment dialog ignores a lookup for a
host the field no longer shows, and the route tests run under it.effect.
HaukeSchnau pushed a commit to HaukeSchnau/t3code that referenced this pull request Nov 14, 2026
…nID Connect provider' (pingdotgg#163) from feat/oidc-sign-in into main
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