Skip to content

Supersedes #408: thread model + effort overrides through app-server spawn path (fixes #476)#2

Merged
axisrow merged 9 commits into
mainfrom
fix-app-server-overrides
Jul 12, 2026
Merged

Supersedes #408: thread model + effort overrides through app-server spawn path (fixes #476)#2
axisrow merged 9 commits into
mainfrom
fix-app-server-overrides

Conversation

@axisrow

@axisrow axisrow commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes the --model/--effort flags being silently ignored on /codex:review and /codex:adversarial-review (openai#476), and supersedes openai#408 by @hiirott by carrying its commits forward onto current main and extending them.

Root cause (verified against codex-cli 0.144.1)

model/effort in the JSON-RPC params (thread/start, turn/start, review/start) are dead: the Rust app-server deserializes without #[serde(deny_unknown_fields)], so unknown fields are silently dropped and the server boots with the CLI's built-in default. The only effective override is -c key=value at codex app-server spawn time.

Measured end-to-end by reading the resolved config from the session turn_context:

mechanism resolved reasoning_effort
baseline (no override) low (default)
effort in turn/start RPC low (dropped — no effect)
-c model_reasoning_effort=high at spawn high

Same pattern for model (-c model="gpt-5.4" → resolved gpt-5.4; RPC field → no effect).

What this PR does

Carries openai#408's two commits (model-only spawn override) onto current main (hiirott's branch was 18 commits behind, base 8e403f9), with authorship preserved, then extends:

  1. Mirrors effort (model_reasoning_effort) across the spawn/broker chain everywhere fix(app-server): pass -c model="..." to codex app-server so options.model takes effect openai/codex-plugin-cc#408 handles model: SpawnedCodexAppServerClient.initialize() argv, connect()ensureBrokerSession, spawnBrokerProcess (--effort), app-server-broker.mjs parseArgs + inner connect, withAppServer clientOptions → runAppServerReview/runAppServerTurn.
  2. Parses --effort on review/adversarial-review (reusing the clean review-command plumbing from the closed Add model and effort flags to review commands openai/codex-plugin-cc#477, but on the working spawn mechanism instead of the dead review/start RPC params). review/start params stay { threadId, delivery, target }.
  3. Closes the stale-broker-reuse gap flagged on fix(app-server): pass -c model="..." to codex app-server so options.model takes effect openai/codex-plugin-cc#408: ensureBrokerSession now stores the model/effort the broker was spawned with and respawns when a per-call override differs (previously a warm broker silently ignored later overrides).
  4. Honest tests: the fake fixture now records the app-server spawn argv (lastAppServerSpawnArgs), so tests prove the override actually reaches codex app-server — not merely that it was forwarded to a dead RPC param. The existing RPC-param assertions (lastTurnStart/lastReviewStart) remain as forwarding smoke tests.

Why this supersedes both prior PRs

Verification

  • node --test tests/commands.test.mjs → 8/8 ✅
  • node --test tests/runtime.test.mjs → new spawn-argv test (RED before the fix, GREEN after) + broker-respawn test both green; all existing broker-reuse tests still green (appServerStarts === 1 reuse preserved). Pre-existing env-related failures in setup/status/result tests are unchanged from main (Test suite is not hermetic inside a live Claude Code session: 4 failures at HEAD + fixture state leaks into the user's real plugin data dir openai/codex-plugin-cc#455 — suite is not hermetic) and unrelated to this change.
  • Manual end-to-end against real codex-cli 0.144.1, reading resolved config from session turn_context:
    • task --model gpt-5.4 → resolved model: "gpt-5.4"
    • task --effort high → resolved reasoning_effort: "high"
    • review --effort high → resolved reasoning_effort: "high" ✅ (previously: silently ignored)

Notes

  • The codex config key is model_reasoning_effort (full), not effort. The broker CLI flag is --effort (short). Getting this wrong silently no-ops.
  • Native /codex:review has no effort field in ReviewStartParams at all, so the spawn override is the only path by which review can honor --effort.

Opening here in the fork first for local CI / coordination before proposing upstream.

@hiirott — your two commits are carried forward verbatim (cherry-picked with authorship preserved); happy to coordinate if you'd rather fold this into openai#408 directly.

hiirott and others added 7 commits July 12, 2026 01:21
…ns.model takes effect

When `runAppServerTurn` / `runAppServerReview` are invoked with a `model`
option, that value was only delivered via the `thread/start` and
`turn/start` requests. However, the spawn of `codex app-server` itself
did not include a `-c model="..."` override, so the codex CLI session
booted with the CLI's built-in default model. On a ChatGPT account where
the default (`gpt-5.3-codex`) is unsupported, every task launched via
`codex-companion.mjs task --background --model <m>` failed with:

```
Codex error: 'gpt-5.3-codex' model is not supported when using Codex
with a ChatGPT account.
```

even though the user passed `--model gpt-5.4`.

This change threads the model option through to the spawn site:

- `lib/app-server.mjs`: when `options.model` is set, add
  `-c model=${JSON.stringify(model)}` to the spawned argv. JSON quoting
  yields a valid TOML string literal that the codex CLI's `-c` parser
  accepts unambiguously (e.g. `model="gpt-5.4"`).
- `lib/codex.mjs` `withAppServer`: accept a third `clientOptions`
  argument and forward it to `CodexAppServerClient.connect` for both
  the primary and the direct-retry connection.
- `lib/codex.mjs` `runAppServerReview` / `runAppServerTurn`: pass
  `{ model: options.model }` through to `withAppServer`.

`interruptAppServerTurn` and `findLatestTaskThread` still create their
own short-lived clients without a model override; they are not exercised
by the failing `task --background --model` path and are left for a
follow-up.

Verified locally on macOS / codex-cli 0.130.0 with:

```
node companion.mjs task --background --fresh --model gpt-5.4 "say hi"
```

Before this patch the job failed during `turn/start`; after, the job
completes in ~7s with the assistant reply.
…kground tasks honor --model

The 2026-06-30 fix (0531356) only covered the direct/fallback client path. The broker
path used by task --background dropped options.model at ensureBrokerSession, so the
broker's long-lived codex app-server child always started on the CLI default model.
Thread model through connect -> ensureBrokerSession -> spawnBrokerProcess ->
app-server-broker serve --model -> inner connect.

RCA: 根因=broker 子プロセス起動時に model が渡っていなかった(6/30 修正は direct 経路のみ) 再発防止=~/.claude/patches/ registry を4ファイル版に更新・README に broker 経路の注意書き

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pawn

Mirrors the -c model= override (openai#408, hiirott) for effort using the
codex config key `model_reasoning_effort`, threaded through the full
spawn/broker chain:
- SpawnedCodexAppServerClient.initialize() argv
- connect() -> ensureBrokerSession
- spawnBrokerProcess argv (--effort) + app-server-broker.mjs parseArgs
- withAppServer clientOptions -> runAppServerReview/Turn

RPC params for model/effort (thread/start, turn/start, review/start) are
dead: the app-server (serde, no deny_unknown_fields) drops them, so the
override must reach codex at spawn time. Verified end-to-end against
codex-cli 0.144.1: resolved reasoning_effort in turn_context reflects the
override.

Co-Authored-By: hiirott <hiiroakitmail@gmail.com>
… to spawn

Reuses the review-command plumbing from openai#477 (axisrow), but on the working
spawn-override mechanism instead of the dead review/start RPC params:
- handleReviewCommand parses --effort, normalizes via normalizeReasoningEffort
  (and normalizes --model via normalizeRequestedModel so `spark` -> gpt-5.3-codex-spark)
- executeReviewRun forwards effort to runAppServerReview and runAppServerTurn

review/start RPC params stay { threadId, delivery, target } (no model/effort) —
the override reaches the server via the spawn argv.

Closes the --effort half of openai#476 for both review and adversarial-review.

Co-Authored-By: axisrow <axisrow@users.noreply.github.com>
Both flags now work via the app-server spawn override, so advertising them
on /codexreview and /codex:adversarial-review is honest. Pins the
argument-hint + "runtime-selection flags" prose in commands.test.mjs.
…erver

The fake codex fixture previously discarded the app-server spawn argv,
so no test could prove a -c override reached codex (the existing
lastTurnStart/lastReviewStart RPC-param assertions are dead for this —
the server drops RPC model/effort). Now persists lastAppServerSpawnArgs.

Adds:
- review --model/--effort reach spawn argv as -c overrides (load-bearing
  effectiveness proof, RED before the spawn-override fix, GREEN after)
- differing --effort on a warm broker respawns it (gap-B fix; asserts
  appServerStarts === 2 and the override in the respawn argv)

Also records lastReviewStart in the fixture (forwarding smoke test).
The stale-broker-respawn path called teardownBrokerSession with
killProcess=null (connect() never passes it) and never sent a
broker/shutdown RPC, so the detached+unref'd broker node process and its
codex app-server child were orphaned indefinitely when a differing
--model/--effort override triggered a respawn. Previously teardown only
ran for a dead broker, so this leak was newly introduced.

Mirror the session-lifecycle-hook pattern: send sendBrokerShutdown first
(graceful — the broker closes its app-server child itself), then pass
killProcess: terminateProcessTree as a SIGKILL fallback.

Verified: across repeated review runs with differing overrides, no
orphaned codex app-server processes accumulate.

Co-Authored-By: Claude <noreply@anthropic.com>
@axisrow

axisrow commented Jul 12, 2026

Copy link
Copy Markdown
Owner Author

🔍 Local review (cycle 1/3)

Reviewed locally (Claude subagent + Codex companion), no bots pinged.

Verdict Reviewer Finding Location
FIX ✅ applied claude Stale broker process leaked on model/effort respawn — detached broker + its codex app-server child orphaned indefinitely plugins/codex/scripts/lib/broker-lifecycle.mjs:135
codex approve, 0 findings

The FIX finding (critical, now fixed in 09b3a41)

On the stale-broker-respawn path (live broker + differing --model/--effort → respawn), teardownBrokerSession was called with killProcess: null (because connect() never passes killProcess) and no broker/shutdown RPC was sent — so the detached+unref'd broker node process and its codex app-server child were never stopped, only their socket file got unlinked. Each differing-override invocation leaked a process pair.

This leak was newly introduced by the gap-B respawn feature: previously teardownBrokerSession only ran when the broker was already dead (endpoint not ready), so the missing kill was harmless.

Verified by reading the code (teardownBrokerSession only kills under if (Number.isFinite(pid) && killProcess); sendBrokerShutdown is called from exactly one place — session-lifecycle-hook.mjs) and empirically: across repeated review runs with differing overrides, no orphaned codex app-server processes accumulate after the fix.

Fix mirrors the existing session-lifecycle-hook pattern: sendBrokerShutdown(endpoint) first (graceful — broker closes its app-server child itself), then killProcess: terminateProcessTree as a SIGKILL fallback.

Codex's approve did not catch this (its review was diff-only and didn't trace the respawn path's process lifecycle); the Claude subagent flagged it, and triage confirmed against the code.

Cycle 1 had a FIX → proceeding to cycle 2 review on the updated diff.

… hang

sendBrokerShutdown awaited the broker's broker/shutdown response with no
timeout. If the existing broker is wedged enough to accept the TCP/Unix
connect but never processes the RPC, emits no data, and never closes the
socket, the promise never resolves — so ensureBrokerSession's respawn
path (and every command that needs a respawn) hangs indefinitely, turning
a stale-broker cleanup into a hard hang.

Add a bounded timeout (default 2s): on expiry, destroy the socket and
resolve so the caller falls back to the terminateProcessTree kill.

Co-Authored-By: Claude <noreply@anthropic.com>
@axisrow

axisrow commented Jul 12, 2026

Copy link
Copy Markdown
Owner Author

🔍 Local review (cycle 2/3)

Reviewed locally (Claude subagent + Codex companion) on the updated diff (after cycle-1's broker-leak fix).

Verdict Reviewer Finding Location
FIX ✅ applied codex sendBrokerShutdown has no timeout → respawn can hang forever plugins/codex/scripts/lib/broker-lifecycle.mjs:44-57
claude 0 findings (missed the hang edge-case)

The FIX finding (high → critical, now fixed in 8458741)

Cycle-1 added await sendBrokerShutdown(existing.endpoint) to the respawn path. Codex (cycle 2) flagged that sendBrokerShutdown awaited the broker's broker/shutdown response with no timeout: if the existing broker is wedged — accepts the connect but never processes the RPC, emits no data, never closes — none of the data/error/close handlers fire and the promise hangs indefinitely, blocking ensureBrokerSession and every command that needs a respawn.

The Claude subagent (cycle 2) cleared it — it reasoned that error/close handlers prevent a hang, but missed the wedged-but-connected case where the socket is open, idle, and silent. Codex's adversarial read caught it; triage confirmed by reading the handler set (no timeout path).

Fix: bound sendBrokerShutdown with a default 2s timeout — on expiry, destroy the socket and resolve so the caller falls back to the terminateProcessTree kill already in the respawn path.

All broker/reuse/gap-B tests still green; the 8 runtime failures are pre-existing env flakes (identical on main).

Cycle 2 had a FIX → proceeding to cycle 3 on the updated diff.

Hardening follow-up to the respawn-path kill: only pass killProcess when
we confirmed the existing broker's endpoint was ready (so the pid still
belongs to our broker). For a stale session whose endpoint is not ready,
the pid likely points at a dead broker whose pid the OS may have reused
for an unrelated process — in that case just drop the socket/session
files and let any survivor exit on its own.

This narrows the PID-reuse surface on the respawn path while preserving
the orphaned-process fix (the live-broker respawn case still tree-kills).

Co-Authored-By: Claude <noreply@anthropic.com>
@axisrow

axisrow commented Jul 12, 2026

Copy link
Copy Markdown
Owner Author

🔍 Local review (cycle 3/3) — clean (no FIX)

Reviewed locally (Claude subagent + Codex companion) on the updated diff (after cycle-2's sendBrokerShutdown timeout fix).

Verdict Reviewer Finding Location
SKIP → minor hardening applied codex PID-reuse risk: respawn-path kill-by-persisted-PID could hit an unrelated process if the broker crashed and the OS reused its pid plugins/codex/scripts/lib/broker-lifecycle.mjs:174
(clean) claude 0 findings

Codex's PID-reuse finding → SKIP, plus a targeted hardening (478be42)

Codex (cycle 3) flagged that the respawn path now kills by the pid persisted in broker.json, which — if the broker crashed and the OS reused that pid for an unrelated process — could signal the wrong process. Verified factually correct, but:

  • The identical kill-by-persisted-pid pattern was already shipping before this PR in session-lifecycle-hook.mjs (the SessionEnd hook, killProcess: terminateProcessTree with pid from broker.json), so this is a pre-existing, established codebase pattern, not a new class of bug introduced here. Our cycle-1 fix explicitly mirrors it. Verdict: SKIP (consistent with existing behavior; holistic PID-identity hardening is a separate cross-cutting follow-up).
  • Still, a cheap targeted hardening landed in 478be42: only tree-kill when the broker's endpoint was confirmed live (existingReady), so a stale session whose endpoint is not ready — the risky PID-reuse case — just drops the files instead of killing by pid. The live-broker respawn case (the orphaned-process leak cycle 1 fixed) still tree-kills. All broker/gap-B tests stay green.

Claude's cycle-3 read cleared the timeout fix (clearTimeout on every path, settled-guard, no remaining hang).

✅ Review cycle complete

3/3 cycles run, no remaining FIX verdicts. Fixes applied across rounds:

  • 09b3a41 — stale broker process leaked on respawn (cycle 1, critical)
  • 8458741sendBrokerShutdown hang → bounded 2s timeout (cycle 2, critical)
  • 478be42 — narrow the respawn-path pid-kill to live brokers only (cycle 3, hardening)

Local review (Claude + Codex) considers the PR merge-ready. Local mode does not auto-merge — merge is yours to trigger.

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.

2 participants