Version: plugin 1.0.6 (codex-companion.mjs), observed on Windows 11, Node 24, codex-cli 0.146.0-alpha.3.1.
Summary. enqueueBackgroundTask spawns the detached task worker before writing the job record the worker requires at startup. If the worker wins the race (or dies at startup for any reason), the parent still writes a queued record and reports the task as started. Nothing ever moves that record to a terminal state, and status reports it as active indefinitely.
Where (scripts/codex-companion.mjs, v1.0.6):
enqueueBackgroundTask calls spawnDetachedTaskWorker(cwd, job.id) at line 688, then writes the job file with writeJobFile(...) at line 697 and upserts the index at line 698.
- The worker (
handleTaskWorker, line 838) immediately reads that job file and throws if it is absent.
- The worker is spawned with
stdio: "ignore" and detached: true (lines 673-679), so its startup failure is invisible to the parent and to the user.
renderQueuedTaskLaunch (line 556) then reports the task as started in the background.
status classifies jobs from the stored status field only (buildStatusSnapshot, lib/job-control.mjs line 213 onward); there is no PID liveness check, so the stuck queued record looks active forever.
Impact. A user who dispatches a background task gets a success message and a job id, but no work is running and no error ever surfaces. The failure mode is indistinguishable from a slow queue until the user inspects the log file (which contains only the "Queued for background execution." line).
Suggested fix. Write the job file and upsert the index BEFORE spawning the worker (swap lines 688-698 so writeJobFile/upsertJob precede spawnDetachedTaskWorker). The worker's read-at-startup then cannot race the write. Optionally: have status reconcile queued/running records whose pid is no longer alive.
Related hardening idea (separate, smaller). A cancel failure on Windows (taskkill returning an unrecognized nonzero, e.g. "operation not supported" under a restricted shell) throws in terminateProcessTree (lib/process.mjs line 97) before any cancellation bookkeeping, which also leaves the record active with no recovery path; a liveness reconciliation in status would cover both cases.
Version: plugin 1.0.6 (codex-companion.mjs), observed on Windows 11, Node 24, codex-cli 0.146.0-alpha.3.1.
Summary.
enqueueBackgroundTaskspawns the detached task worker before writing the job record the worker requires at startup. If the worker wins the race (or dies at startup for any reason), the parent still writes aqueuedrecord and reports the task as started. Nothing ever moves that record to a terminal state, andstatusreports it as active indefinitely.Where (scripts/codex-companion.mjs, v1.0.6):
enqueueBackgroundTaskcallsspawnDetachedTaskWorker(cwd, job.id)at line 688, then writes the job file withwriteJobFile(...)at line 697 and upserts the index at line 698.handleTaskWorker, line 838) immediately reads that job file and throws if it is absent.stdio: "ignore"anddetached: true(lines 673-679), so its startup failure is invisible to the parent and to the user.renderQueuedTaskLaunch(line 556) then reports the task as started in the background.statusclassifies jobs from the storedstatusfield only (buildStatusSnapshot, lib/job-control.mjs line 213 onward); there is no PID liveness check, so the stuckqueuedrecord looks active forever.Impact. A user who dispatches a background task gets a success message and a job id, but no work is running and no error ever surfaces. The failure mode is indistinguishable from a slow queue until the user inspects the log file (which contains only the "Queued for background execution." line).
Suggested fix. Write the job file and upsert the index BEFORE spawning the worker (swap lines 688-698 so
writeJobFile/upsertJobprecedespawnDetachedTaskWorker). The worker's read-at-startup then cannot race the write. Optionally: havestatusreconcilequeued/runningrecords whosepidis no longer alive.Related hardening idea (separate, smaller). A cancel failure on Windows (
taskkillreturning an unrecognized nonzero, e.g. "operation not supported" under a restricted shell) throws interminateProcessTree(lib/process.mjs line 97) before any cancellation bookkeeping, which also leaves the record active with no recovery path; a liveness reconciliation instatuswould cover both cases.