Repository navigation
Description
Summary
When a Codex task process crashes or is killed externally, its job record remains in status: "running" permanently. All subsequent task calls in the same Claude session are rejected with:
Task {job-id} is still running. Use /codex:status before continuing it.
This effectively blocks all Codex usage in that session until the user manually fixes the state file.
Steps to Reproduce
- Start a Codex task via
/codex:rescue(foreground or background) - The task process crashes or is killed (e.g., OOM, signal, Codex app-server timeout)
- Job record in
state.jsonremainsstatus: "running"with a dead PID - All subsequent
taskcalls in the same session fail immediately with "Task is still running"
Root Cause
In codex-companion.mjs, resolveLatestTrackedTaskThread() checks for active tasks:
const activeTask = visibleJobs.find(
(job) => job.jobClass === "task" &&
(job.status === "queued" || job.status === "running")
);
if (activeTask) {
throw new Error(`Task ${activeTask.id} is still running.`);
}This only checks the status field in the job record. It does not verify whether the process (stored in job.pid) is actually alive.
Evidence from Production
Observed in plugin version 1.0.3. A session had 6 consecutive failed task attempts, all blocked by the same zombie job:
task-mntm7f2j status=running pid=82311 (process DEAD)
task-mntmbrox status=failed error="Task task-mntm7f2j is still running"
task-mntmbvx4 status=failed error="Task task-mntm7f2j is still running"
task-mntmclwl status=failed error="Task task-mntm7f2j is still running"
task-mntme0qz status=failed error="Task task-mntm7f2j is still running"
task-mntmg2an status=failed error="Task task-mntm7f2j is still running"
task-mntnft5z status=failed error="Task task-mntm7f2j is still running"
The PID was confirmed dead via kill(82311, 0) → ProcessLookupError.
Suggested Fix
In resolveLatestTrackedTaskThread(), before throwing, verify the PID is alive. If the process is dead, mark the job as failed and continue:
const activeTask = visibleJobs.find(
(job) => job.jobClass === "task" &&
(job.status === "queued" || job.status === "running")
);
if (activeTask) {
// Check if the process is actually alive
const pid = activeTask.pid;
let processAlive = false;
if (pid) {
try {
process.kill(pid, 0);
processAlive = true;
} catch (e) {
processAlive = (e.code === 'EPERM'); // exists but no permission
}
}
if (processAlive) {
throw new Error(`Task ${activeTask.id} is still running.`);
}
// Process is dead — mark as failed (zombie cleanup)
upsertJob(workspaceRoot, {
id: activeTask.id,
status: "failed",
phase: "failed",
pid: null,
errorMessage: "Process exited without updating job status.",
completedAt: new Date().toISOString()
});
}Environment
- Plugin version: 1.0.3
- Codex CLI:
@openai/codex(latest) - Claude Code: 2.1.89
- Platform: macOS (Darwin 24.6.0, arm64)
- Node.js: v22.12.0
Activity
Fix openai#202: zombie job blocks subsequent task calls when process …
fix: handle zombie jobs when process crashes but status remains "runn…
D2758695161 commentedon Apr 14, 2026
Fix implemented in PR #218. The root cause was that resolveLatestTrackedTaskThread() only checked job.status without verifying the process was actually alive.
2488583886 commentedon Apr 14, 2026
Additional Root Cause: Foreground
await state.completionhangs forever on IPC disconnectThe zombie job state file (described in the original issue) is one symptom, but there's a more fundamental bug that causes foreground tasks to hang indefinitely — the Claude Code session becomes completely unresponsive with no way to recover.
Root Cause
In
scripts/lib/codex.mjs,captureTurn()(line ~600):state.completionis a standalone Promise created increateTurnCaptureState(). It is only resolved when aturn/completednotification arrives via the IPC notification handler.When the app-server process dies (OOM, SIGKILL, broker socket disconnect, etc.):
handleExit()fires inapp-server.mjs→ rejects allclient.pendingRPC requests ✅client.exitPromiseresolves ✅state.completionis never resolved or rejected ❌ — it's a completely separate Promise that nobody touches on connection deathawait state.completionhangs forever → Node.js process never exits → Bash call never returns → Claude Code is stuckSuggested Fix
Race
state.completionagainstclient.exitPromiseso that IPC disconnect immediately unblocks the caller:This is complementary to the PID liveness check proposed in the original issue — that fix handles background zombie jobs (stale state file), while this fix handles foreground hangs (Promise never settling). Both are needed for full coverage.
Impact
Without this fix, any foreground Codex task (
/codex:rescue,/codex:review) will permanently freeze the Claude Code session if the app-server dies mid-turn. The user has no way to recover except killing the entire Claude Code process.fix: reconcile stale 'running' jobs when tracked PID is dead
fix: reconcile stale 'running' jobs when tracked PID is dead
fix(broker): P0 stability — idle timeout + PID liveness + shutdown ti…
fix(state): reap stale running jobs via OS-level PID liveness check (o…
dkundel-openai commentedon Aug 11, 2026
Duplicate of #222.