fix: deduplicate Codex log followers

This commit is contained in:
liujing
2026-07-20 00:55:33 +08:00
parent 71b8025666
commit 84f1dc589e
5 changed files with 305 additions and 56 deletions
+77 -55
View File
@@ -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);
}
}