From c6397249e7787e284c3b734602d1ea7c6a692c96 Mon Sep 17 00:00:00 2001 From: "Sparsh :)" <76697238+SparshGarg999@users.noreply.github.com> Date: Sun, 5 Jul 2026 08:42:06 +0530 Subject: [PATCH 1/2] Fix background job zombie entries and enhance --fresh clearing --- plugins/codex/scripts/codex-companion.mjs | 4 + plugins/codex/scripts/lib/state.mjs | 27 +++++- tests/state.test.mjs | 104 +++++++++++++++++++++- 3 files changed, 132 insertions(+), 3 deletions(-) diff --git a/plugins/codex/scripts/codex-companion.mjs b/plugins/codex/scripts/codex-companion.mjs index 83df468a..f58c9092 100644 --- a/plugins/codex/scripts/codex-companion.mjs +++ b/plugins/codex/scripts/codex-companion.mjs @@ -27,6 +27,7 @@ import { collectReviewContext, ensureGitRepository, resolveReviewTarget } from " import { binaryAvailable, terminateProcessTree } from "./lib/process.mjs"; import { loadPromptTemplate, interpolateTemplate } from "./lib/prompts.mjs"; import { + clearTerminalJobs, generateJobId, getConfig, listJobs, @@ -779,6 +780,9 @@ async function handleTask(argv) { if (resumeLast && fresh) { throw new Error("Choose either --resume/--resume-last or --fresh."); } + if (fresh) { + clearTerminalJobs(cwd); + } const write = Boolean(options.write); const taskMetadata = buildTaskRunMetadata({ prompt, diff --git a/plugins/codex/scripts/lib/state.mjs b/plugins/codex/scripts/lib/state.mjs index 2da23498..83f0f5d3 100644 --- a/plugins/codex/scripts/lib/state.mjs +++ b/plugins/codex/scripts/lib/state.mjs @@ -147,7 +147,32 @@ export function upsertJob(cwd, jobPatch) { } export function listJobs(cwd) { - return loadState(cwd).jobs; + const state = loadState(cwd); + const now = Date.now(); + const nextJobs = state.jobs.filter((job) => { + if (job.status !== "running" && job.updatedAt) { + const updatedTime = Date.parse(job.updatedAt); + if (Number.isFinite(updatedTime) && now - updatedTime > 30 * 60 * 1000) { + return false; + } + } + return true; + }); + if (nextJobs.length !== state.jobs.length) { + state.jobs = nextJobs; + saveState(cwd, state); + } + return nextJobs; +} + +export function clearTerminalJobs(cwd) { + const state = loadState(cwd); + const nextJobs = state.jobs.filter((job) => job.status === "running"); + if (nextJobs.length !== state.jobs.length) { + state.jobs = nextJobs; + saveState(cwd, state); + } + return nextJobs; } export function setConfig(cwd, key, value) { diff --git a/tests/state.test.mjs b/tests/state.test.mjs index 0f8f57ce..022615db 100644 --- a/tests/state.test.mjs +++ b/tests/state.test.mjs @@ -4,8 +4,8 @@ import path from "node:path"; import test from "node:test"; import assert from "node:assert/strict"; -import { makeTempDir } from "./helpers.mjs"; -import { resolveJobFile, resolveJobLogFile, resolveStateDir, resolveStateFile, saveState } from "../plugins/codex/scripts/lib/state.mjs"; +import { makeTempDir, initGitRepo } from "./helpers.mjs"; +import { resolveJobFile, resolveJobLogFile, resolveStateDir, resolveStateFile, saveState, listJobs, clearTerminalJobs } from "../plugins/codex/scripts/lib/state.mjs"; test("resolveStateDir uses a temp-backed per-workspace directory", () => { const workspace = makeTempDir(); @@ -42,6 +42,7 @@ test("resolveStateDir uses CLAUDE_PLUGIN_DATA when it is provided", () => { test("saveState prunes dropped job artifacts when indexed jobs exceed the cap", () => { const workspace = makeTempDir(); + initGitRepo(workspace); const stateFile = resolveStateFile(workspace); fs.mkdirSync(path.dirname(stateFile), { recursive: true }); @@ -103,3 +104,102 @@ test("saveState prunes dropped job artifacts when indexed jobs exceed the cap", .sort() ); }); + +test("listJobs automatically garbage collects completed/failed jobs older than 30 minutes", () => { + const workspace = makeTempDir(); + initGitRepo(workspace); + const stateFile = resolveStateFile(workspace); + fs.mkdirSync(path.dirname(stateFile), { recursive: true }); + + const now = Date.now(); + const jobs = [ + { + id: "job-running-old", + status: "running", + updatedAt: new Date(now - 40 * 60 * 1000).toISOString(), + createdAt: new Date(now - 40 * 60 * 1000).toISOString() + }, + { + id: "job-completed-old", + status: "completed", + updatedAt: new Date(now - 40 * 60 * 1000).toISOString(), + createdAt: new Date(now - 40 * 60 * 1000).toISOString() + }, + { + id: "job-completed-fresh", + status: "completed", + updatedAt: new Date(now - 10 * 60 * 1000).toISOString(), + createdAt: new Date(now - 10 * 60 * 1000).toISOString() + } + ]; + + for (const job of jobs) { + fs.writeFileSync(resolveJobFile(workspace, job.id), JSON.stringify(job), "utf8"); + fs.writeFileSync(resolveJobLogFile(workspace, job.id), "log\n", "utf8"); + } + + saveState(workspace, { + version: 1, + config: { stopReviewGate: false }, + jobs + }); + + // Old completed job should be GC'ed, old running job and fresh completed job should remain. + const activeJobs = listJobs(workspace); + assert.equal(activeJobs.length, 2); + assert.equal(activeJobs.some((j) => j.id === "job-running-old"), true); + assert.equal(activeJobs.some((j) => j.id === "job-completed-fresh"), true); + assert.equal(activeJobs.some((j) => j.id === "job-completed-old"), false); + + assert.equal(fs.existsSync(resolveJobFile(workspace, "job-completed-old")), false); + assert.equal(fs.existsSync(resolveJobFile(workspace, "job-completed-fresh")), true); + assert.equal(fs.existsSync(resolveJobFile(workspace, "job-running-old")), true); +}); + +test("clearTerminalJobs removes all completed/failed/stopped jobs but keeps running ones", () => { + const workspace = makeTempDir(); + initGitRepo(workspace); + const stateFile = resolveStateFile(workspace); + fs.mkdirSync(path.dirname(stateFile), { recursive: true }); + + const now = Date.now(); + const jobs = [ + { + id: "job-running", + status: "running", + updatedAt: new Date(now).toISOString(), + createdAt: new Date(now).toISOString() + }, + { + id: "job-completed", + status: "completed", + updatedAt: new Date(now).toISOString(), + createdAt: new Date(now).toISOString() + }, + { + id: "job-failed", + status: "failed", + updatedAt: new Date(now).toISOString(), + createdAt: new Date(now).toISOString() + } + ]; + + for (const job of jobs) { + fs.writeFileSync(resolveJobFile(workspace, job.id), JSON.stringify(job), "utf8"); + fs.writeFileSync(resolveJobLogFile(workspace, job.id), "log\n", "utf8"); + } + + saveState(workspace, { + version: 1, + config: { stopReviewGate: false }, + jobs + }); + + const activeJobs = clearTerminalJobs(workspace); + assert.equal(activeJobs.length, 1); + assert.equal(activeJobs[0].id, "job-running"); + + assert.equal(fs.existsSync(resolveJobFile(workspace, "job-completed")), false); + assert.equal(fs.existsSync(resolveJobFile(workspace, "job-failed")), false); + assert.equal(fs.existsSync(resolveJobFile(workspace, "job-running")), true); +}); From b6ae0ea4a13e9f45246fe3e1b0b971cbbefbffb9 Mon Sep 17 00:00:00 2001 From: "Sparsh :)" <76697238+SparshGarg999@users.noreply.github.com> Date: Mon, 6 Jul 2026 12:55:22 +0530 Subject: [PATCH 2/2] Restrict background job garbage collection and fresh clearing to explicit terminal statuses, preserving queued jobs --- plugins/codex/scripts/lib/state.mjs | 6 ++++-- tests/state.test.mjs | 24 ++++++++++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/plugins/codex/scripts/lib/state.mjs b/plugins/codex/scripts/lib/state.mjs index 83f0f5d3..85f2f6fe 100644 --- a/plugins/codex/scripts/lib/state.mjs +++ b/plugins/codex/scripts/lib/state.mjs @@ -149,8 +149,9 @@ export function upsertJob(cwd, jobPatch) { export function listJobs(cwd) { const state = loadState(cwd); const now = Date.now(); + const terminalStatuses = ["completed", "failed", "cancelled", "stopped"]; const nextJobs = state.jobs.filter((job) => { - if (job.status !== "running" && job.updatedAt) { + if (terminalStatuses.includes(job.status) && job.updatedAt) { const updatedTime = Date.parse(job.updatedAt); if (Number.isFinite(updatedTime) && now - updatedTime > 30 * 60 * 1000) { return false; @@ -167,7 +168,8 @@ export function listJobs(cwd) { export function clearTerminalJobs(cwd) { const state = loadState(cwd); - const nextJobs = state.jobs.filter((job) => job.status === "running"); + const terminalStatuses = ["completed", "failed", "cancelled", "stopped"]; + const nextJobs = state.jobs.filter((job) => !terminalStatuses.includes(job.status)); if (nextJobs.length !== state.jobs.length) { state.jobs = nextJobs; saveState(cwd, state); diff --git a/tests/state.test.mjs b/tests/state.test.mjs index 022615db..84bfda35 100644 --- a/tests/state.test.mjs +++ b/tests/state.test.mjs @@ -119,6 +119,12 @@ test("listJobs automatically garbage collects completed/failed jobs older than 3 updatedAt: new Date(now - 40 * 60 * 1000).toISOString(), createdAt: new Date(now - 40 * 60 * 1000).toISOString() }, + { + id: "job-queued-old", + status: "queued", + updatedAt: new Date(now - 40 * 60 * 1000).toISOString(), + createdAt: new Date(now - 40 * 60 * 1000).toISOString() + }, { id: "job-completed-old", status: "completed", @@ -144,16 +150,18 @@ test("listJobs automatically garbage collects completed/failed jobs older than 3 jobs }); - // Old completed job should be GC'ed, old running job and fresh completed job should remain. + // Old completed job should be GC'ed, old running/queued and fresh completed jobs should remain. const activeJobs = listJobs(workspace); - assert.equal(activeJobs.length, 2); + assert.equal(activeJobs.length, 3); assert.equal(activeJobs.some((j) => j.id === "job-running-old"), true); + assert.equal(activeJobs.some((j) => j.id === "job-queued-old"), true); assert.equal(activeJobs.some((j) => j.id === "job-completed-fresh"), true); assert.equal(activeJobs.some((j) => j.id === "job-completed-old"), false); assert.equal(fs.existsSync(resolveJobFile(workspace, "job-completed-old")), false); assert.equal(fs.existsSync(resolveJobFile(workspace, "job-completed-fresh")), true); assert.equal(fs.existsSync(resolveJobFile(workspace, "job-running-old")), true); + assert.equal(fs.existsSync(resolveJobFile(workspace, "job-queued-old")), true); }); test("clearTerminalJobs removes all completed/failed/stopped jobs but keeps running ones", () => { @@ -170,6 +178,12 @@ test("clearTerminalJobs removes all completed/failed/stopped jobs but keeps runn updatedAt: new Date(now).toISOString(), createdAt: new Date(now).toISOString() }, + { + id: "job-queued", + status: "queued", + updatedAt: new Date(now).toISOString(), + createdAt: new Date(now).toISOString() + }, { id: "job-completed", status: "completed", @@ -196,10 +210,12 @@ test("clearTerminalJobs removes all completed/failed/stopped jobs but keeps runn }); const activeJobs = clearTerminalJobs(workspace); - assert.equal(activeJobs.length, 1); - assert.equal(activeJobs[0].id, "job-running"); + assert.equal(activeJobs.length, 2); + assert.equal(activeJobs.some((j) => j.id === "job-running"), true); + assert.equal(activeJobs.some((j) => j.id === "job-queued"), true); assert.equal(fs.existsSync(resolveJobFile(workspace, "job-completed")), false); assert.equal(fs.existsSync(resolveJobFile(workspace, "job-failed")), false); assert.equal(fs.existsSync(resolveJobFile(workspace, "job-running")), true); + assert.equal(fs.existsSync(resolveJobFile(workspace, "job-queued")), true); });