Summary
Four defects found while diagnosing reliability issues with background jobs on Windows. All verified against plugin source v1.0.6 with session logs and on-disk job state. Environment: Windows 11, plugin 1.0.6, codex CLI 0.148.0 / 0.150.1.
1. Natural-language prompt words are swallowed as CLI options (-m pytest → model pytest)
When a caller passes a task as a single raw argument string, splitRawArgumentString (scripts/lib/args.mjs:76-127) tokenizes it shell-style, and parseArgs then interprets -m via aliasMap: { m: "model" } (codex-companion.mjs:764-768). A prompt containing python -m pytest tests parses to options.model = "pytest", which normalizeRequestedModel (codex-companion.mjs:103-111) passes through unchanged, producing gateway 404s:
Model "pytest" is not supported
Reproduced 3× (rollout turn_context with model="pytest" at 3 timestamps; corresponding job logs show the 404s). This is version-independent (parsing happens in the plugin).
Suggested fix: don't shell-split structured input; pass prompts as a dedicated field or require -- separation; drop or rename the m alias; validate model values against the known catalog before sending.
Workaround: avoid bare -m in prompt text, or use --prompt-file.
2. Dead background workers are never finalized (zombie running jobs)
Background jobs spawn detached workers with stdio: "ignore" + child.unref() (codex-companion.mjs:671-682), and job state is written after spawn (684-698). If the worker dies at any point, nothing marks the job failed — tracked-jobs.mjs:142-203 only finalizes on normal runner return/throw, and there is no watchdog, PID liveness check, or timeout reclaim (job-control.mjs:281-307 treats queued/running as active indefinitely).
On-disk evidence: jobs created days apart still running with dead PIDs; one from Aug 21 whose log contains only Starting Codex Resume.. Two jobs from different workspaces/brokers stopped at the same second (network-level kill event) and both remain running forever.
Suggested fix: worker heartbeat or PID liveness checks; queued/running timeout reclaim; write a terminal state whenever the worker process is observed dead.
3. cancel hangs because it awaits an interrupt with no timeout
handleCancel awaits interruptAppServerTurn before killing the process tree (codex-companion.mjs:963-987), with no timeout wrapper (codex.mjs:960-992). If the broker/turn is already gone, the await blocks indefinitely; terminateProcessTree and the status write never execute. Corroborating evidence: the entire job-state directory contains zero cancelled records despite explicit user cancels. Minor secondary issue: terminateProcessTree(job.pid ?? NaN) when pid is absent.
Suggested fix: short timeout on interrupt → force-kill process tree on expiry → always write a terminal state; skip the interrupt path entirely for jobs whose PID is already dead.
4. All worker stderr is discarded (zero observability)
stdio: "ignore" means codex CLI / app-server stderr (connection drops, retry storms, panics) is never persisted. This made root-causing #2 impossible from local evidence — the actual death cause of the same-second worker deaths could not be determined because all evidence was destroyed by design.
Suggested fix: persist worker stdout/stderr to the job log (even best-effort tail), at least around spawn/connect/turn lifecycle transitions.
Workarounds in use
Foreground mode for critical tasks; avoid bare -m in prompts; periodic cleanup of zombie job files and orphaned cxc-* temp dirs.
Summary
Four defects found while diagnosing reliability issues with background jobs on Windows. All verified against plugin source v1.0.6 with session logs and on-disk job state. Environment: Windows 11, plugin 1.0.6, codex CLI 0.148.0 / 0.150.1.
1. Natural-language prompt words are swallowed as CLI options (
-m pytest→ modelpytest)When a caller passes a task as a single raw argument string,
splitRawArgumentString(scripts/lib/args.mjs:76-127) tokenizes it shell-style, andparseArgsthen interprets-mviaaliasMap: { m: "model" }(codex-companion.mjs:764-768). A prompt containingpython -m pytest testsparses tooptions.model = "pytest", whichnormalizeRequestedModel(codex-companion.mjs:103-111) passes through unchanged, producing gateway 404s:Reproduced 3× (rollout turn_context with
model="pytest"at 3 timestamps; corresponding job logs show the 404s). This is version-independent (parsing happens in the plugin).Suggested fix: don't shell-split structured input; pass prompts as a dedicated field or require
--separation; drop or rename themalias; validate model values against the known catalog before sending.Workaround: avoid bare
-min prompt text, or use--prompt-file.2. Dead background workers are never finalized (zombie
runningjobs)Background jobs spawn detached workers with
stdio: "ignore"+child.unref()(codex-companion.mjs:671-682), and job state is written after spawn (684-698). If the worker dies at any point, nothing marks the job failed —tracked-jobs.mjs:142-203only finalizes on normal runner return/throw, and there is no watchdog, PID liveness check, or timeout reclaim (job-control.mjs:281-307treatsqueued/runningas active indefinitely).On-disk evidence: jobs created days apart still
runningwith dead PIDs; one from Aug 21 whose log contains onlyStarting Codex Resume.. Two jobs from different workspaces/brokers stopped at the same second (network-level kill event) and both remainrunningforever.Suggested fix: worker heartbeat or PID liveness checks;
queued/runningtimeout reclaim; write a terminal state whenever the worker process is observed dead.3.
cancelhangs because it awaits an interrupt with no timeouthandleCancelawaitsinterruptAppServerTurnbefore killing the process tree (codex-companion.mjs:963-987), with no timeout wrapper (codex.mjs:960-992). If the broker/turn is already gone, the await blocks indefinitely;terminateProcessTreeand the status write never execute. Corroborating evidence: the entire job-state directory contains zerocancelledrecords despite explicit user cancels. Minor secondary issue:terminateProcessTree(job.pid ?? NaN)when pid is absent.Suggested fix: short timeout on interrupt → force-kill process tree on expiry → always write a terminal state; skip the interrupt path entirely for jobs whose PID is already dead.
4. All worker stderr is discarded (zero observability)
stdio: "ignore"means codex CLI / app-server stderr (connection drops, retry storms, panics) is never persisted. This made root-causing #2 impossible from local evidence — the actual death cause of the same-second worker deaths could not be determined because all evidence was destroyed by design.Suggested fix: persist worker stdout/stderr to the job log (even best-effort tail), at least around spawn/connect/turn lifecycle transitions.
Workarounds in use
Foreground mode for critical tasks; avoid bare
-min prompts; periodic cleanup of zombie job files and orphanedcxc-*temp dirs.