@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
22import fs from "node:fs" ;
33import os from "node:os" ;
44import path from "node:path" ;
5+ import process from "node:process" ;
56
67import { resolveWorkspaceRoot } from "./workspace.mjs" ;
78
@@ -16,6 +17,94 @@ function nowIso() {
1617 return new Date ( ) . toISOString ( ) ;
1718}
1819
20+ function isProcessAlive ( pidValue ) {
21+ const pid = Number ( pidValue ) ;
22+ if ( ! Number . isFinite ( pid ) || pid <= 0 ) {
23+ return false ;
24+ }
25+
26+ try {
27+ process . kill ( Math . trunc ( pid ) , 0 ) ;
28+ return true ;
29+ } catch ( error ) {
30+ if ( error && typeof error === "object" && "code" in error && error . code === "EPERM" ) {
31+ return true ;
32+ }
33+ return false ;
34+ }
35+ }
36+
37+ function normalizePid ( pidValue ) {
38+ const pid = Number ( pidValue ) ;
39+ if ( ! Number . isFinite ( pid ) || pid <= 0 ) {
40+ return null ;
41+ }
42+ return Math . trunc ( pid ) ;
43+ }
44+
45+ function appendStaleJobLog ( job , message ) {
46+ if ( ! job ?. logFile ) {
47+ return ;
48+ }
49+ try {
50+ fs . appendFileSync ( job . logFile , `[${ nowIso ( ) } ] ${ message } \n` , "utf8" ) ;
51+ } catch {
52+ // Best-effort logging; status reconciliation should not fail on log write errors.
53+ }
54+ }
55+
56+ function reconcileRunningJobs ( cwd , jobs ) {
57+ const completedAt = nowIso ( ) ;
58+ let changed = false ;
59+
60+ const nextJobs = jobs . map ( ( job ) => {
61+ if ( job ?. status !== "running" ) {
62+ return job ;
63+ }
64+ const pid = normalizePid ( job . pid ) ;
65+ if ( pid == null ) {
66+ return job ;
67+ }
68+ if ( isProcessAlive ( pid ) ) {
69+ return job ;
70+ }
71+
72+ changed = true ;
73+ const reason = `process ${ pid } is not running` ;
74+ const errorMessage = `Codex job ended unexpectedly (${ reason } ); auto-reconciled as failed.` ;
75+ const nextJob = {
76+ ...job ,
77+ status : "failed" ,
78+ phase : "failed" ,
79+ pid : null ,
80+ completedAt,
81+ errorMessage,
82+ updatedAt : completedAt
83+ } ;
84+
85+ appendStaleJobLog ( job , `Detected stale running job (${ reason } ). Marked as failed automatically.` ) ;
86+ const jobFile = resolveJobFile ( cwd , job . id ) ;
87+ if ( fs . existsSync ( jobFile ) ) {
88+ try {
89+ const stored = readJobFile ( jobFile ) ;
90+ writeJobFile ( cwd , job . id , {
91+ ...stored ,
92+ ...nextJob
93+ } ) ;
94+ } catch {
95+ // Ignore malformed on-disk job files; state reconciliation still proceeds.
96+ }
97+ }
98+
99+ return nextJob ;
100+ } ) ;
101+
102+ return {
103+ changed,
104+ jobs : nextJobs
105+ } ;
106+ }
107+
19108function defaultState ( ) {
20109 return {
21110 version : STATE_VERSION ,
@@ -147,7 +236,15 @@ export function upsertJob(cwd, jobPatch) {
147236}
148237
149238export function listJobs ( cwd ) {
150- return loadState ( cwd ) . jobs ;
239+ const state = loadState ( cwd ) ;
240+ const reconciled = reconcileRunningJobs ( cwd , state . jobs ?? [ ] ) ;
241+ if ( reconciled . changed ) {
242+ saveState ( cwd , {
243+ ...state ,
244+ jobs : reconciled . jobs
245+ } ) ;
246+ }
247+ return reconciled . jobs ;
151248}
152249
153250export function setConfig ( cwd , key , value ) {
0 commit comments