fix: deduplicate Codex log followers
This commit is contained in:
+77
-55
@@ -48,9 +48,12 @@ import {
|
||||
findRunDir,
|
||||
generateRunId,
|
||||
initWorkflowState,
|
||||
acquireLogsFollowLock,
|
||||
LogsFollowLockError,
|
||||
nowIso,
|
||||
readState,
|
||||
readProgressSidecar,
|
||||
releaseLogsFollowLock,
|
||||
releaseLock,
|
||||
runDir as makeRunDir,
|
||||
writeState,
|
||||
@@ -1944,70 +1947,89 @@ export async function logsWorkflow(opts: LogsWorkflowOpts): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Tail the last N lines
|
||||
const lines = readTailLines(logPath, opts.tail);
|
||||
for (const line of lines) {
|
||||
process.stdout.write(line + "\n");
|
||||
let followLockPath: string | undefined;
|
||||
if (opts.follow) {
|
||||
const codexThreadId = process.env.CODEX_THREAD_ID?.trim();
|
||||
if (codexThreadId) {
|
||||
try {
|
||||
followLockPath = acquireLogsFollowLock(dir, codexThreadId);
|
||||
} catch (err) {
|
||||
if (err instanceof LogsFollowLockError) {
|
||||
throw new WorkflowEngineError(err.message, 4);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!opts.follow) return;
|
||||
|
||||
// Follow mode: watch for new data and retry rotations
|
||||
let currentPath = logPath;
|
||||
let lastByteOffset = fs.statSync(currentPath).size;
|
||||
|
||||
// States that indicate execution has ceased (no more output expected)
|
||||
const terminalStates = new Set([
|
||||
"completed", "needs_human", "blocked", "budget_exhausted", "aborted",
|
||||
"awaiting_review", "awaiting_host", "awaiting_scope_resolution",
|
||||
]);
|
||||
|
||||
while (true) {
|
||||
const curState = readState(dir);
|
||||
if (!curState) break;
|
||||
|
||||
// Exit if the workflow has stopped executing
|
||||
if (terminalStates.has(curState.status)) {
|
||||
drainBytes(currentPath, lastByteOffset);
|
||||
return;
|
||||
try {
|
||||
// Tail the last N lines
|
||||
const lines = readTailLines(logPath, opts.tail);
|
||||
for (const line of lines) {
|
||||
process.stdout.write(line + "\n");
|
||||
}
|
||||
|
||||
// Check for log rotation to a newer attempt
|
||||
const sc = readProgressSidecar(dir);
|
||||
let candidatePath: string | undefined;
|
||||
if (sc?.logFilePath && fs.existsSync(path.join(dir, sc.logFilePath))) {
|
||||
candidatePath = path.join(dir, sc.logFilePath);
|
||||
} else {
|
||||
const curCycle = curState.cycles.find((c) => c.cycleIndex === curState.currentCycle);
|
||||
if (curCycle?.executorLogFile) {
|
||||
candidatePath = path.join(dir, curCycle.executorLogFile);
|
||||
}
|
||||
}
|
||||
if (!opts.follow) return;
|
||||
|
||||
if (candidatePath && candidatePath !== currentPath) {
|
||||
drainBytes(currentPath, lastByteOffset);
|
||||
currentPath = candidatePath;
|
||||
lastByteOffset = fs.statSync(currentPath).size;
|
||||
process.stdout.write(`\n--- log rotated to ${path.basename(currentPath)} ---\n`);
|
||||
const headLines = readTailLines(currentPath, Math.min(opts.tail, 10));
|
||||
for (const line of headLines) {
|
||||
process.stdout.write(line + "\n");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Follow mode: watch for new data and retry rotations
|
||||
let currentPath = logPath;
|
||||
let lastByteOffset = fs.statSync(currentPath).size;
|
||||
|
||||
// Drain new bytes since lastByteOffset using byte-accurate reading
|
||||
try {
|
||||
const stats = fs.statSync(currentPath);
|
||||
if (stats.size > lastByteOffset) {
|
||||
// States that indicate execution has ceased (no more output expected)
|
||||
const terminalStates = new Set([
|
||||
"completed", "needs_human", "blocked", "budget_exhausted", "aborted",
|
||||
"awaiting_review", "awaiting_host", "awaiting_scope_resolution",
|
||||
]);
|
||||
|
||||
while (true) {
|
||||
const curState = readState(dir);
|
||||
if (!curState) break;
|
||||
|
||||
// Exit if the workflow has stopped executing
|
||||
if (terminalStates.has(curState.status)) {
|
||||
drainBytes(currentPath, lastByteOffset);
|
||||
lastByteOffset = stats.size;
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// File may have been deleted or rotated
|
||||
}
|
||||
|
||||
await sleepMs(500);
|
||||
// Check for log rotation to a newer attempt
|
||||
const sc = readProgressSidecar(dir);
|
||||
let candidatePath: string | undefined;
|
||||
if (sc?.logFilePath && fs.existsSync(path.join(dir, sc.logFilePath))) {
|
||||
candidatePath = path.join(dir, sc.logFilePath);
|
||||
} else {
|
||||
const curCycle = curState.cycles.find((c) => c.cycleIndex === curState.currentCycle);
|
||||
if (curCycle?.executorLogFile) {
|
||||
candidatePath = path.join(dir, curCycle.executorLogFile);
|
||||
}
|
||||
}
|
||||
|
||||
if (candidatePath && candidatePath !== currentPath) {
|
||||
drainBytes(currentPath, lastByteOffset);
|
||||
currentPath = candidatePath;
|
||||
lastByteOffset = fs.statSync(currentPath).size;
|
||||
process.stdout.write(`\n--- log rotated to ${path.basename(currentPath)} ---\n`);
|
||||
const headLines = readTailLines(currentPath, Math.min(opts.tail, 10));
|
||||
for (const line of headLines) {
|
||||
process.stdout.write(line + "\n");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Drain new bytes since lastByteOffset using byte-accurate reading
|
||||
try {
|
||||
const stats = fs.statSync(currentPath);
|
||||
if (stats.size > lastByteOffset) {
|
||||
drainBytes(currentPath, lastByteOffset);
|
||||
lastByteOffset = stats.size;
|
||||
}
|
||||
} catch {
|
||||
// File may have been deleted or rotated
|
||||
}
|
||||
|
||||
await sleepMs(500);
|
||||
}
|
||||
} finally {
|
||||
if (followLockPath) releaseLogsFollowLock(followLockPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user