Fix chat type persistence and board flow
This commit is contained in:
@@ -29,6 +29,14 @@ const runnerLogTrimIntervalMs = Math.max(
|
||||
Number(process.env.SERVER_COMMAND_RUNNER_LOG_TRIM_INTERVAL_MS?.trim() || '60000'),
|
||||
);
|
||||
const STREAM_CAPTURE_LIMIT = 256 * 1024;
|
||||
const CODEX_LIVE_IDLE_TIMEOUT_MS = Math.max(
|
||||
30_000,
|
||||
Number(process.env.SERVER_COMMAND_RUNNER_CODEX_IDLE_TIMEOUT_MS?.trim() || '90000'),
|
||||
);
|
||||
const CODEX_LIVE_MAX_EXECUTION_MS = Math.max(
|
||||
CODEX_LIVE_IDLE_TIMEOUT_MS,
|
||||
Number(process.env.SERVER_COMMAND_RUNNER_CODEX_MAX_EXECUTION_MS?.trim() || `${10 * 60 * 1000}`),
|
||||
);
|
||||
const CODEX_HOME_RUNTIME_PATHS = [
|
||||
'auth.json',
|
||||
'config.toml',
|
||||
@@ -342,12 +350,24 @@ function collectCodexTextFragments(value) {
|
||||
return Object.values(record).flatMap((item) => collectCodexTextFragments(item));
|
||||
}
|
||||
|
||||
function extractCompletedAgentMessageText(item) {
|
||||
if (!item || typeof item !== 'object') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (item.type !== 'agent_message') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return collectCodexTextFragments(item.text ?? item.content ?? item.message).join('');
|
||||
}
|
||||
|
||||
function extractCodexStreamText(parsed) {
|
||||
const type = typeof parsed.type === 'string' ? parsed.type : '';
|
||||
|
||||
if (type === 'item.completed') {
|
||||
return {
|
||||
completedText: collectCodexTextFragments(parsed.item).join(''),
|
||||
completedText: extractCompletedAgentMessageText(parsed.item),
|
||||
deltaText: '',
|
||||
};
|
||||
}
|
||||
@@ -504,6 +524,9 @@ async function runCodexLiveExecution(payload, response) {
|
||||
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',
|
||||
@@ -540,6 +563,68 @@ async function runCodexLiveExecution(payload, response) {
|
||||
await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
||||
};
|
||||
|
||||
const clearExecutionTimers = () => {
|
||||
if (idleTimer) {
|
||||
clearTimeout(idleTimer);
|
||||
idleTimer = null;
|
||||
}
|
||||
|
||||
if (executionTimer) {
|
||||
clearTimeout(executionTimer);
|
||||
executionTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
const requestTermination = (message) => {
|
||||
if (terminationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
terminationRequested = true;
|
||||
clearExecutionTimers();
|
||||
|
||||
if (!responseClosed) {
|
||||
sendJsonLine(response, {
|
||||
type: 'error',
|
||||
message,
|
||||
});
|
||||
response.end();
|
||||
responseClosed = true;
|
||||
}
|
||||
|
||||
child.kill('SIGTERM');
|
||||
setTimeout(() => {
|
||||
if (!child.killed) {
|
||||
child.kill('SIGKILL');
|
||||
}
|
||||
}, 3000).unref?.();
|
||||
};
|
||||
|
||||
const refreshIdleTimer = () => {
|
||||
if (terminationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (idleTimer) {
|
||||
clearTimeout(idleTimer);
|
||||
}
|
||||
|
||||
idleTimer = setTimeout(() => {
|
||||
requestTermination(
|
||||
`Codex Live 실행이 ${Math.round(CODEX_LIVE_IDLE_TIMEOUT_MS / 1000)}초 동안 출력이 없어 중단되었습니다.`,
|
||||
);
|
||||
}, CODEX_LIVE_IDLE_TIMEOUT_MS);
|
||||
idleTimer.unref?.();
|
||||
};
|
||||
|
||||
executionTimer = setTimeout(() => {
|
||||
requestTermination(
|
||||
`Codex Live 실행이 ${Math.round(CODEX_LIVE_MAX_EXECUTION_MS / 1000)}초를 넘어 중단되었습니다.`,
|
||||
);
|
||||
}, CODEX_LIVE_MAX_EXECUTION_MS);
|
||||
executionTimer.unref?.();
|
||||
refreshIdleTimer();
|
||||
|
||||
const handleCodexJsonLine = (line) => {
|
||||
let parsed;
|
||||
|
||||
@@ -552,6 +637,7 @@ async function runCodexLiveExecution(payload, response) {
|
||||
const activityLog = extractCodexActivityLog(parsed);
|
||||
|
||||
if (activityLog) {
|
||||
refreshIdleTimer();
|
||||
sendJsonLine(response, {
|
||||
type: 'activity',
|
||||
line: activityLog,
|
||||
@@ -561,6 +647,7 @@ async function runCodexLiveExecution(payload, response) {
|
||||
const { completedText: nextCompletedText, deltaText } = extractCodexStreamText(parsed);
|
||||
|
||||
if (nextCompletedText) {
|
||||
refreshIdleTimer();
|
||||
completedText = nextCompletedText;
|
||||
sendJsonLine(response, {
|
||||
type: 'completed',
|
||||
@@ -570,6 +657,7 @@ async function runCodexLiveExecution(payload, response) {
|
||||
}
|
||||
|
||||
if (deltaText) {
|
||||
refreshIdleTimer();
|
||||
sendJsonLine(response, {
|
||||
type: 'delta',
|
||||
text: deltaText,
|
||||
@@ -582,9 +670,11 @@ async function runCodexLiveExecution(payload, response) {
|
||||
|
||||
response.on('close', () => {
|
||||
responseClosed = true;
|
||||
clearExecutionTimers();
|
||||
});
|
||||
|
||||
child.stdout?.on('data', (chunk) => {
|
||||
refreshIdleTimer();
|
||||
const text = String(chunk);
|
||||
stdoutTail = (stdoutTail + text).slice(-STREAM_CAPTURE_LIMIT);
|
||||
jsonLineBuffer += text;
|
||||
@@ -612,6 +702,7 @@ async function runCodexLiveExecution(payload, response) {
|
||||
});
|
||||
|
||||
child.stderr?.on('data', (chunk) => {
|
||||
refreshIdleTimer();
|
||||
const text = String(chunk);
|
||||
stderrTail = (stderrTail + text).slice(-STREAM_CAPTURE_LIMIT);
|
||||
text
|
||||
@@ -631,6 +722,7 @@ async function runCodexLiveExecution(payload, response) {
|
||||
});
|
||||
|
||||
child.on('error', async (error) => {
|
||||
clearExecutionTimers();
|
||||
if (!responseClosed) {
|
||||
sendJsonLine(response, {
|
||||
type: 'error',
|
||||
@@ -643,6 +735,7 @@ async function runCodexLiveExecution(payload, response) {
|
||||
});
|
||||
|
||||
child.on('close', async (code) => {
|
||||
clearExecutionTimers();
|
||||
const trailingLine = jsonLineBuffer.trim();
|
||||
if (trailingLine) {
|
||||
handleCodexJsonLine(trailingLine);
|
||||
|
||||
69
scripts/server-command-runner-supervisor.sh
Executable file
69
scripts/server-command-runner-supervisor.sh
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
PROJECT_ROOT="${PROJECT_ROOT:-$(pwd)}"
|
||||
RUNNER_SCRIPT="${SERVER_COMMAND_RUNNER_SCRIPT:-$PROJECT_ROOT/scripts/run-server-command-runner.mjs}"
|
||||
RUNNER_NODE_BIN="${SERVER_COMMAND_RUNNER_NODE_BIN:-node}"
|
||||
SUPERVISOR_PID_FILE="${SERVER_COMMAND_RUNNER_SUPERVISOR_PID_FILE:-/tmp/server-command-runner-supervisor.pid}"
|
||||
CHILD_PID=""
|
||||
STOP_REQUESTED="0"
|
||||
RELOAD_REQUESTED="0"
|
||||
|
||||
log() {
|
||||
printf '[server-command-runner-supervisor] %s\n' "$*"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -f "$SUPERVISOR_PID_FILE"
|
||||
}
|
||||
|
||||
start_child() {
|
||||
log "starting runner child"
|
||||
"$RUNNER_NODE_BIN" "$RUNNER_SCRIPT" &
|
||||
CHILD_PID=$!
|
||||
}
|
||||
|
||||
request_reload() {
|
||||
RELOAD_REQUESTED="1"
|
||||
log "reload requested"
|
||||
if [ -n "$CHILD_PID" ]; then
|
||||
kill -TERM "$CHILD_PID" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
request_stop() {
|
||||
STOP_REQUESTED="1"
|
||||
log "shutdown requested"
|
||||
if [ -n "$CHILD_PID" ]; then
|
||||
kill -TERM "$CHILD_PID" 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
trap 'request_reload' HUP
|
||||
trap 'request_stop' INT TERM
|
||||
trap 'cleanup' EXIT
|
||||
|
||||
printf '%s\n' "$$" >"$SUPERVISOR_PID_FILE"
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
while :; do
|
||||
start_child
|
||||
set +e
|
||||
wait "$CHILD_PID"
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
CHILD_PID=""
|
||||
|
||||
if [ "$STOP_REQUESTED" = "1" ]; then
|
||||
exit "$EXIT_CODE"
|
||||
fi
|
||||
|
||||
if [ "$RELOAD_REQUESTED" = "1" ]; then
|
||||
RELOAD_REQUESTED="0"
|
||||
continue
|
||||
fi
|
||||
|
||||
log "runner exited unexpectedly with code $EXIT_CODE; restarting in 2 seconds"
|
||||
sleep 2
|
||||
done
|
||||
Reference in New Issue
Block a user