openai / openai/codex-plugin-cc

result/cancel report "No job found" for jobs that exist; killed workers stay `running` forever

Open
#639 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
33.3k
Forks
2.3k
PR merge metrics
No merged PRs in 30d

Description

Two issues in the background-job lifecycle. First found on plugin 1.0.2 (8e403f9); re-checked
against 1.0.6 (db52e28) on 2026-08-13 with codex-cli 0.144.1 on macOS and both are unchanged
lib/tracked-jobs.mjs is byte-identical between the two releases, and in lib/job-control.mjs
neither matchJobReference nor resolveResultJob was touched. They compound: together they make a
job that died indistinguishable from one that is healthy.

1. matchJobReference throws "No job found" whenever its filter misses, not when the job is missing

resolveResultJob (lib/job-control.mjs:256) selects with a finished-only predicate:

const selected = matchJobReference(jobs, reference,
  (job) => job.status === "completed" || job.status === "failed" || job.status === "cancelled");

but matchJobReference throws rather than returning null when a reference is supplied and the
filtered set has no match (:210). It returns null only when no reference is given (:193-195):

if (!reference) {                  // :193
  return filtered[0] ?? null;      // :194 — the only null-returning path
}
...
throw new Error(`No job found for "${reference}". Run /codex:status to list known jobs.`);   // :210

So the friendlier branch immediately below — which appears written for exactly this case — is
unreachable whenever a job id is passed:

const active = matchJobReference(jobs, reference, (j) => j.status === "queued" || j.status === "running");
if (active) throw new Error(`Job ${active.id} is still ${active.status}. ...`);   // :269-272

The same shape affects cancel, and 1.0.6 added a second unreachable branch to it.
resolveCancelableJob (:281) pre-filters to queued/running and then calls matchJobReference
on that set, so a finished job id misses the filter and throws at :210 — before the new
if (!selected) guard at :288-290 can run:

const selected = matchJobReference(activeJobs, reference);        // :287 — throws for a finished id
if (!selected) {
  throw new Error(`No active job found for "${reference}".`);     // :289 — never reached
}

Repro, measured on 1.0.6 on 2026-08-13 — the same job id, seconds apart, same state directory:

$ node scripts/codex-companion.mjs status --all
- task-msq886m0-6cdtmt | completed | rescue | Codex Task
  Phase: done

$ node scripts/codex-companion.mjs cancel task-msq886m0-6cdtmt
No job found for "task-msq886m0-6cdtmt". Run /codex:status to list known jobs.
$ echo $?
1

Note which message came back: it is matchJobReference's, not the No active job found for "…"
added at :289 — direct evidence that the newer branch is dead code.

The running-job case has the same cause and was measured on 1.0.2: status --all listed the job as
running with a healthy worker pid at the same moment result <job-id> reported it missing. Both
functions involved are unchanged in 1.0.6, so that reading transfers.

Impact: the message is indistinguishable from a genuinely evicted job, so an automated caller that
polls concludes the task was lost and re-dispatches over a review that is still running.

Suggested fix: have matchJobReference return null when a predicate is supplied and let each
caller own its own messaging — the two callers above already have the right message written and
merely cannot reach it. Alternatively, check the active set before the finished set in
resolveResultJob.

2. No liveness reconciliation — a killed worker leaves status: running forever

Only the worker itself writes the running -> completed|failed transition (runTrackedJob,
lib/tracked-jobs.mjs:142). If it is SIGKILLed, OOM-killed, or the machine reboots, the record stays
running permanently — nothing anywhere checks. status --wait polls the same job.status field, so
it waits forever too. A grep across 1.0.6's scripts/ for any process.kill(pid, 0)-style probe
returns nothing.

The job record already stores pid (enqueueBackgroundTask, and again from inside the worker at
lib/tracked-jobs.mjs:148), so the information needed is on disk.

Evidence, re-derived on 2026-08-13: of 222 job records on this machine, exactly one is still
queued/runningreview-molfjlg1-rltrp7, timestamped 2026-04-30, holding pid 89940, which
kill(pid, 0) reports as gone. It has read running for 105 days. Its log ends mid-investigation
with no error.

Suggested fix: in buildStatusSnapshot / resolveResultJob, treat an active record whose pid is
gone as failed — or surface it distinctly (e.g. stale) rather than as running.

Why these two together are worse than either alone

Combined with SessionEnd's cleanupSessionJobs (session-lifecycle-hook.mjs:42, unchanged in
1.0.6 apart from an unrelated import) — which SIGTERMs in-flight jobs of the ending session and then
removes their rows, deleting the .json and .log via saveState (lib/state.mjs:92) — a review
that dies mid-run leaves no trace at all, and result then reports the same "No job found" it
reports for perfectly healthy jobs. From the caller's side the three states (healthy, dead, purged)
are one message.

Happy to send a PR for either if the suggested direction looks right.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with matchJobReference, resolveResultJob, resolveCancelableJob, and buildStatusSnapshot in lib/job-control.mjs, then inspect runTrackedJob and pid persistence in lib/tracked-jobs.mjs. Reproduce the result and cancel commands against a known job and check the status polling path. Done means finished-job references reach their caller-specific errors and jobs whose worker pid is gone no longer remain indefinitely running.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
backend, cli
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.