chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user