205 lines
5.9 KiB
JavaScript
205 lines
5.9 KiB
JavaScript
import fs from "node:fs";
|
|
import process from "node:process";
|
|
|
|
import { readJobFile, resolveJobFile, resolveJobLogFile, upsertJob, writeJobFile } from "./state.mjs";
|
|
|
|
export const SESSION_ID_ENV = "CODEX_COMPANION_SESSION_ID";
|
|
|
|
export function nowIso() {
|
|
return new Date().toISOString();
|
|
}
|
|
|
|
function normalizeProgressEvent(value) {
|
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
return {
|
|
message: String(value.message ?? "").trim(),
|
|
phase: typeof value.phase === "string" && value.phase.trim() ? value.phase.trim() : null,
|
|
threadId: typeof value.threadId === "string" && value.threadId.trim() ? value.threadId.trim() : null,
|
|
turnId: typeof value.turnId === "string" && value.turnId.trim() ? value.turnId.trim() : null,
|
|
stderrMessage: value.stderrMessage == null ? null : String(value.stderrMessage).trim(),
|
|
logTitle: typeof value.logTitle === "string" && value.logTitle.trim() ? value.logTitle.trim() : null,
|
|
logBody: value.logBody == null ? null : String(value.logBody).trimEnd()
|
|
};
|
|
}
|
|
|
|
return {
|
|
message: String(value ?? "").trim(),
|
|
phase: null,
|
|
threadId: null,
|
|
turnId: null,
|
|
stderrMessage: String(value ?? "").trim(),
|
|
logTitle: null,
|
|
logBody: null
|
|
};
|
|
}
|
|
|
|
export function appendLogLine(logFile, message) {
|
|
const normalized = String(message ?? "").trim();
|
|
if (!logFile || !normalized) {
|
|
return;
|
|
}
|
|
fs.appendFileSync(logFile, `[${nowIso()}] ${normalized}\n`, "utf8");
|
|
}
|
|
|
|
export function appendLogBlock(logFile, title, body) {
|
|
if (!logFile || !body) {
|
|
return;
|
|
}
|
|
fs.appendFileSync(logFile, `\n[${nowIso()}] ${title}\n${String(body).trimEnd()}\n`, "utf8");
|
|
}
|
|
|
|
export function createJobLogFile(workspaceRoot, jobId, title) {
|
|
const logFile = resolveJobLogFile(workspaceRoot, jobId);
|
|
fs.writeFileSync(logFile, "", "utf8");
|
|
if (title) {
|
|
appendLogLine(logFile, `Starting ${title}.`);
|
|
}
|
|
return logFile;
|
|
}
|
|
|
|
export function createJobRecord(base, options = {}) {
|
|
const env = options.env ?? process.env;
|
|
const sessionId = env[options.sessionIdEnv ?? SESSION_ID_ENV];
|
|
return {
|
|
...base,
|
|
createdAt: nowIso(),
|
|
...(sessionId ? { sessionId } : {})
|
|
};
|
|
}
|
|
|
|
export function createJobProgressUpdater(workspaceRoot, jobId) {
|
|
let lastPhase = null;
|
|
let lastThreadId = null;
|
|
let lastTurnId = null;
|
|
|
|
return (event) => {
|
|
const normalized = normalizeProgressEvent(event);
|
|
const patch = { id: jobId };
|
|
let changed = false;
|
|
|
|
if (normalized.phase && normalized.phase !== lastPhase) {
|
|
lastPhase = normalized.phase;
|
|
patch.phase = normalized.phase;
|
|
changed = true;
|
|
}
|
|
|
|
if (normalized.threadId && normalized.threadId !== lastThreadId) {
|
|
lastThreadId = normalized.threadId;
|
|
patch.threadId = normalized.threadId;
|
|
changed = true;
|
|
}
|
|
|
|
if (normalized.turnId && normalized.turnId !== lastTurnId) {
|
|
lastTurnId = normalized.turnId;
|
|
patch.turnId = normalized.turnId;
|
|
changed = true;
|
|
}
|
|
|
|
if (!changed) {
|
|
return;
|
|
}
|
|
|
|
upsertJob(workspaceRoot, patch);
|
|
|
|
const jobFile = resolveJobFile(workspaceRoot, jobId);
|
|
if (!fs.existsSync(jobFile)) {
|
|
return;
|
|
}
|
|
|
|
const storedJob = readJobFile(jobFile);
|
|
writeJobFile(workspaceRoot, jobId, {
|
|
...storedJob,
|
|
...patch
|
|
});
|
|
};
|
|
}
|
|
|
|
export function createProgressReporter({ stderr = false, logFile = null, onEvent = null } = {}) {
|
|
if (!stderr && !logFile && !onEvent) {
|
|
return null;
|
|
}
|
|
|
|
return (eventOrMessage) => {
|
|
const event = normalizeProgressEvent(eventOrMessage);
|
|
const stderrMessage = event.stderrMessage ?? event.message;
|
|
if (stderr && stderrMessage) {
|
|
process.stderr.write(`[codex] ${stderrMessage}\n`);
|
|
}
|
|
appendLogLine(logFile, event.message);
|
|
appendLogBlock(logFile, event.logTitle, event.logBody);
|
|
onEvent?.(event);
|
|
};
|
|
}
|
|
|
|
function readStoredJobOrNull(workspaceRoot, jobId) {
|
|
const jobFile = resolveJobFile(workspaceRoot, jobId);
|
|
if (!fs.existsSync(jobFile)) {
|
|
return null;
|
|
}
|
|
return readJobFile(jobFile);
|
|
}
|
|
|
|
export async function runTrackedJob(job, runner, options = {}) {
|
|
const runningRecord = {
|
|
...job,
|
|
status: "running",
|
|
startedAt: nowIso(),
|
|
phase: "starting",
|
|
pid: process.pid,
|
|
logFile: options.logFile ?? job.logFile ?? null
|
|
};
|
|
writeJobFile(job.workspaceRoot, job.id, runningRecord);
|
|
upsertJob(job.workspaceRoot, runningRecord);
|
|
|
|
try {
|
|
const execution = await runner();
|
|
const completionStatus = execution.exitStatus === 0 ? "completed" : "failed";
|
|
const completedAt = nowIso();
|
|
writeJobFile(job.workspaceRoot, job.id, {
|
|
...runningRecord,
|
|
status: completionStatus,
|
|
threadId: execution.threadId ?? null,
|
|
turnId: execution.turnId ?? null,
|
|
pid: null,
|
|
phase: completionStatus === "completed" ? "done" : "failed",
|
|
completedAt,
|
|
result: execution.payload,
|
|
rendered: execution.rendered
|
|
});
|
|
upsertJob(job.workspaceRoot, {
|
|
id: job.id,
|
|
status: completionStatus,
|
|
threadId: execution.threadId ?? null,
|
|
turnId: execution.turnId ?? null,
|
|
summary: execution.summary,
|
|
phase: completionStatus === "completed" ? "done" : "failed",
|
|
pid: null,
|
|
completedAt
|
|
});
|
|
appendLogBlock(options.logFile ?? job.logFile ?? null, "Final output", execution.rendered);
|
|
return execution;
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
const existing = readStoredJobOrNull(job.workspaceRoot, job.id) ?? runningRecord;
|
|
const completedAt = nowIso();
|
|
writeJobFile(job.workspaceRoot, job.id, {
|
|
...existing,
|
|
status: "failed",
|
|
phase: "failed",
|
|
errorMessage,
|
|
pid: null,
|
|
completedAt,
|
|
logFile: options.logFile ?? job.logFile ?? existing.logFile ?? null
|
|
});
|
|
upsertJob(job.workspaceRoot, {
|
|
id: job.id,
|
|
status: "failed",
|
|
phase: "failed",
|
|
pid: null,
|
|
errorMessage,
|
|
completedAt
|
|
});
|
|
throw error;
|
|
}
|
|
}
|