Skip to content

task --background: detached worker is spawned before its job file is written, and a fast-failing worker leaves a permanently queued job that was reported as started #620

Description

@VOXDConsulting

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions