Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions plugins/codex/scripts/lib/state.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,36 @@ export function listJobs(cwd) {
return loadState(cwd).jobs;
}

/**
* Check whether a process with the given pid is currently alive.
* Cross-platform: uses `process.kill(pid, 0)` which sends signal 0
* (no-op signal) and throws ESRCH if the process is gone. EPERM means
* the process exists but we lack permission to signal it (still alive).
*/
export function isPidAlive(pid) {
if (!pid || typeof pid !== "number") return false;
try {
process.kill(pid, 0);
return true;
} catch (err) {
return err && err.code === "EPERM";
}
}

/**
* A job counts as "active" only if its status says queued/running AND
* the recorded pid is still alive. This prevents zombie state from
* causing false alarms when the codex process was SIGKILL'd
* (e.g., by a cleanup script or an OS reboot) without getting a chance
* to write status="completed" to disk.
*/
export function isJobActive(job) {
if (!job) return false;
if (job.status !== "queued" && job.status !== "running") return false;
if (!job.pid) return false;
return isPidAlive(job.pid);
}

export function setConfig(cwd, key, value) {
return updateState(cwd, (state) => {
state.config = {
Expand Down
4 changes: 2 additions & 2 deletions plugins/codex/scripts/stop-review-gate-hook.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { fileURLToPath } from "node:url";

import { getCodexAvailability } from "./lib/codex.mjs";
import { loadPromptTemplate, interpolateTemplate } from "./lib/prompts.mjs";
import { getConfig, listJobs } from "./lib/state.mjs";
import { getConfig, isJobActive, listJobs } from "./lib/state.mjs";
import { sortJobsNewestFirst } from "./lib/job-control.mjs";
import { SESSION_ID_ENV } from "./lib/tracked-jobs.mjs";
import { resolveWorkspaceRoot } from "./lib/workspace.mjs";
Expand Down Expand Up @@ -146,7 +146,7 @@ function main() {
const config = getConfig(workspaceRoot);

const jobs = sortJobsNewestFirst(filterJobsForCurrentSession(listJobs(workspaceRoot), input));
const runningJob = jobs.find((job) => job.status === "queued" || job.status === "running");
const runningJob = jobs.find(isJobActive);
const runningTaskNote = runningJob
? `Codex task ${runningJob.id} is still running. Check /codex:status and use /codex:cancel ${runningJob.id} if you want to stop it before ending the session.`
: null;
Expand Down