chore: exclude local resource artifacts from main sync
This commit is contained in:
@@ -51,7 +51,100 @@ const CODEX_HOME_RUNTIME_PATHS = [
|
||||
'version.json',
|
||||
];
|
||||
const CHAT_SESSION_RESOURCE_DIR_MODE = 0o777;
|
||||
const CODEX_LIVE_EVENT_HISTORY_LIMIT = 400;
|
||||
const CODEX_LIVE_FINISHED_RETENTION_MS = Math.max(
|
||||
60_000,
|
||||
Number(process.env.SERVER_COMMAND_RUNNER_CODEX_FINISHED_RETENTION_MS?.trim() || `${10 * 60 * 1000}`),
|
||||
);
|
||||
const activeCodexExecutions = new Map();
|
||||
const recentCodexExecutions = new Map();
|
||||
|
||||
function createCodexExecutionRecord({ requestId, child, tempDir }) {
|
||||
return {
|
||||
requestId,
|
||||
child,
|
||||
tempDir,
|
||||
subscribers: new Set(),
|
||||
history: [],
|
||||
completed: false,
|
||||
cleanupTimer: null,
|
||||
};
|
||||
}
|
||||
|
||||
function registerSubscriberClose(record, response) {
|
||||
response.on('close', () => {
|
||||
record.subscribers.delete(response);
|
||||
});
|
||||
}
|
||||
|
||||
function attachCodexExecutionSubscriber(record, response) {
|
||||
response.writeHead(200, {
|
||||
'content-type': 'application/x-ndjson; charset=utf-8',
|
||||
'cache-control': 'no-store',
|
||||
});
|
||||
record.subscribers.add(response);
|
||||
registerSubscriberClose(record, response);
|
||||
|
||||
for (const payload of record.history) {
|
||||
sendJsonLine(response, payload);
|
||||
}
|
||||
|
||||
if (record.completed) {
|
||||
response.end();
|
||||
record.subscribers.delete(response);
|
||||
}
|
||||
}
|
||||
|
||||
function broadcastCodexExecutionEvent(record, payload) {
|
||||
record.history.push(payload);
|
||||
if (record.history.length > CODEX_LIVE_EVENT_HISTORY_LIMIT) {
|
||||
record.history.splice(0, record.history.length - CODEX_LIVE_EVENT_HISTORY_LIMIT);
|
||||
}
|
||||
|
||||
for (const response of record.subscribers) {
|
||||
try {
|
||||
sendJsonLine(response, payload);
|
||||
} catch {
|
||||
record.subscribers.delete(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeCodexExecutionSubscribers(record) {
|
||||
for (const response of record.subscribers) {
|
||||
try {
|
||||
response.end();
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
record.subscribers.clear();
|
||||
}
|
||||
|
||||
function scheduleCodexExecutionCleanup(record) {
|
||||
if (record.cleanupTimer) {
|
||||
clearTimeout(record.cleanupTimer);
|
||||
}
|
||||
|
||||
record.cleanupTimer = setTimeout(async () => {
|
||||
recentCodexExecutions.delete(record.requestId);
|
||||
await rm(record.tempDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
}, CODEX_LIVE_FINISHED_RETENTION_MS);
|
||||
record.cleanupTimer.unref?.();
|
||||
}
|
||||
|
||||
function finalizeCodexExecution(record) {
|
||||
if (record.completed) {
|
||||
return;
|
||||
}
|
||||
|
||||
record.completed = true;
|
||||
activeCodexExecutions.delete(record.requestId);
|
||||
recentCodexExecutions.set(record.requestId, record);
|
||||
closeCodexExecutionSubscribers(record);
|
||||
scheduleCodexExecutionCleanup(record);
|
||||
}
|
||||
|
||||
async function ensureWorldWritableDirectory(absolutePath) {
|
||||
await mkdir(absolutePath, { recursive: true, mode: CHAT_SESSION_RESOURCE_DIR_MODE });
|
||||
@@ -559,6 +652,18 @@ async function runCodexLiveExecution(payload, response) {
|
||||
return;
|
||||
}
|
||||
|
||||
const existingExecution = activeCodexExecutions.get(requestId) ?? recentCodexExecutions.get(requestId);
|
||||
|
||||
if (existingExecution) {
|
||||
attachCodexExecutionSubscriber(existingExecution, response);
|
||||
broadcastCodexExecutionEvent(existingExecution, {
|
||||
type: 'attached',
|
||||
requestId,
|
||||
completed: existingExecution.completed,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await validateCodexExecutionRuntime(repoPath, codexBin);
|
||||
await ensureWritableChatSessionDirectories(repoPath, sessionId);
|
||||
|
||||
@@ -568,16 +673,10 @@ async function runCodexLiveExecution(payload, response) {
|
||||
let stderrTail = '';
|
||||
let jsonLineBuffer = '';
|
||||
let completedText = '';
|
||||
let responseClosed = false;
|
||||
let idleTimer = null;
|
||||
let executionTimer = null;
|
||||
let terminationRequested = false;
|
||||
|
||||
response.writeHead(200, {
|
||||
'content-type': 'application/x-ndjson; charset=utf-8',
|
||||
'cache-control': 'no-store',
|
||||
});
|
||||
|
||||
const child = spawn(
|
||||
codexBin,
|
||||
['exec', '--model', CODEX_LIVE_MODEL, '--json', '--dangerously-bypass-approvals-and-sandbox', '-C', repoPath, '-'],
|
||||
@@ -594,22 +693,20 @@ async function runCodexLiveExecution(payload, response) {
|
||||
},
|
||||
);
|
||||
|
||||
activeCodexExecutions.set(requestId, {
|
||||
const executionRecord = createCodexExecutionRecord({
|
||||
requestId,
|
||||
child,
|
||||
tempDir,
|
||||
});
|
||||
sendJsonLine(response, {
|
||||
activeCodexExecutions.set(requestId, executionRecord);
|
||||
attachCodexExecutionSubscriber(executionRecord, response);
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'started',
|
||||
pid: child.pid ?? null,
|
||||
configuredIdleTimeoutSeconds: Math.round(configuredIdleTimeoutMs / 1000),
|
||||
configuredMaxExecutionSeconds: Math.round(configuredMaxExecutionMs / 1000),
|
||||
});
|
||||
|
||||
const cleanup = async () => {
|
||||
activeCodexExecutions.delete(requestId);
|
||||
await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
};
|
||||
|
||||
const clearExecutionTimers = () => {
|
||||
if (idleTimer) {
|
||||
clearTimeout(idleTimer);
|
||||
@@ -630,14 +727,10 @@ async function runCodexLiveExecution(payload, response) {
|
||||
terminationRequested = true;
|
||||
clearExecutionTimers();
|
||||
|
||||
if (!responseClosed) {
|
||||
sendJsonLine(response, {
|
||||
type: 'error',
|
||||
message,
|
||||
});
|
||||
response.end();
|
||||
responseClosed = true;
|
||||
}
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'error',
|
||||
message,
|
||||
});
|
||||
|
||||
child.kill('SIGTERM');
|
||||
setTimeout(() => {
|
||||
@@ -685,7 +778,7 @@ async function runCodexLiveExecution(payload, response) {
|
||||
|
||||
if (activityLog) {
|
||||
refreshIdleTimer();
|
||||
sendJsonLine(response, {
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'activity',
|
||||
line: activityLog,
|
||||
});
|
||||
@@ -696,7 +789,7 @@ async function runCodexLiveExecution(payload, response) {
|
||||
if (nextCompletedText) {
|
||||
refreshIdleTimer();
|
||||
completedText = nextCompletedText;
|
||||
sendJsonLine(response, {
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'completed',
|
||||
text: nextCompletedText,
|
||||
});
|
||||
@@ -705,7 +798,7 @@ async function runCodexLiveExecution(payload, response) {
|
||||
|
||||
if (deltaText) {
|
||||
refreshIdleTimer();
|
||||
sendJsonLine(response, {
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'delta',
|
||||
text: deltaText,
|
||||
});
|
||||
@@ -715,11 +808,6 @@ async function runCodexLiveExecution(payload, response) {
|
||||
return false;
|
||||
};
|
||||
|
||||
response.on('close', () => {
|
||||
responseClosed = true;
|
||||
clearExecutionTimers();
|
||||
});
|
||||
|
||||
child.stdout?.on('data', (chunk) => {
|
||||
refreshIdleTimer();
|
||||
const text = String(chunk);
|
||||
@@ -740,7 +828,7 @@ async function runCodexLiveExecution(payload, response) {
|
||||
}
|
||||
|
||||
if (!line.startsWith('{') && !isIgnorableCodexDiagnosticLine(line)) {
|
||||
sendJsonLine(response, {
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'stdout',
|
||||
line,
|
||||
});
|
||||
@@ -761,7 +849,7 @@ async function runCodexLiveExecution(payload, response) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendJsonLine(response, {
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'stderr',
|
||||
line,
|
||||
});
|
||||
@@ -770,15 +858,15 @@ async function runCodexLiveExecution(payload, response) {
|
||||
|
||||
child.on('error', async (error) => {
|
||||
clearExecutionTimers();
|
||||
if (!responseClosed) {
|
||||
sendJsonLine(response, {
|
||||
type: 'error',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
response.end();
|
||||
}
|
||||
|
||||
await cleanup();
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'error',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'finished',
|
||||
exitCode: null,
|
||||
});
|
||||
finalizeCodexExecution(executionRecord);
|
||||
});
|
||||
|
||||
child.on('close', async (code) => {
|
||||
@@ -788,18 +876,18 @@ async function runCodexLiveExecution(payload, response) {
|
||||
handleCodexJsonLine(trailingLine);
|
||||
}
|
||||
|
||||
if (!responseClosed) {
|
||||
if (code !== 0) {
|
||||
sendJsonLine(response, {
|
||||
type: 'error',
|
||||
message: summarizeCodexOutput(`${stderrTail}\n${completedText}\n${stdoutTail}`),
|
||||
});
|
||||
}
|
||||
|
||||
response.end();
|
||||
if (code !== 0) {
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'error',
|
||||
message: summarizeCodexOutput(`${stderrTail}\n${completedText}\n${stdoutTail}`),
|
||||
});
|
||||
}
|
||||
|
||||
await cleanup();
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'finished',
|
||||
exitCode: code ?? null,
|
||||
});
|
||||
finalizeCodexExecution(executionRecord);
|
||||
});
|
||||
|
||||
child.stdin?.end(prompt);
|
||||
@@ -958,6 +1046,28 @@ const server = createServer(async (request, response) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const attachMatch = requestUrl.pathname.match(/^\/api\/codex-live\/jobs\/([^/]+)\/attach$/);
|
||||
if (request.method === 'POST' && attachMatch) {
|
||||
const requestId = decodeURIComponent(attachMatch[1]);
|
||||
const execution = activeCodexExecutions.get(requestId) ?? recentCodexExecutions.get(requestId);
|
||||
|
||||
if (!execution) {
|
||||
sendJson(response, 404, {
|
||||
attached: false,
|
||||
message: '재부착할 Codex 작업을 찾지 못했습니다.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
attachCodexExecutionSubscriber(execution, response);
|
||||
broadcastCodexExecutionEvent(execution, {
|
||||
type: 'attached',
|
||||
requestId,
|
||||
completed: execution.completed,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const cancelMatch = requestUrl.pathname.match(/^\/api\/codex-live\/jobs\/([^/]+)\/cancel$/);
|
||||
if (request.method === 'POST' && cancelMatch) {
|
||||
const requestId = decodeURIComponent(cancelMatch[1]);
|
||||
|
||||
Reference in New Issue
Block a user