chore: sync backend and deployment changes
This commit is contained in:
@@ -59,6 +59,11 @@ const CODEX_LIVE_FINISHED_RETENTION_MS = Math.max(
|
||||
const activeCodexExecutions = new Map();
|
||||
const recentCodexExecutions = new Map();
|
||||
|
||||
function resolveCodexLiveModel(value) {
|
||||
const normalized = String(value ?? '').trim();
|
||||
return /^[A-Za-z0-9._:-]+$/u.test(normalized) ? normalized : CODEX_LIVE_MODEL;
|
||||
}
|
||||
|
||||
function createCodexExecutionRecord({ requestId, child, tempDir }) {
|
||||
return {
|
||||
requestId,
|
||||
@@ -134,6 +139,99 @@ function scheduleCodexExecutionCleanup(record) {
|
||||
record.cleanupTimer.unref?.();
|
||||
}
|
||||
|
||||
function normalizeCodexUsageMetricValue(value) {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return Math.max(0, Math.round(value));
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
const normalized = Number(value);
|
||||
if (Number.isFinite(normalized)) {
|
||||
return Math.max(0, Math.round(normalized));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractCodexUsageSnapshot(parsed) {
|
||||
if (!parsed || typeof parsed !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const usage =
|
||||
parsed.usage && typeof parsed.usage === 'object'
|
||||
? parsed.usage
|
||||
: parsed.response &&
|
||||
typeof parsed.response === 'object' &&
|
||||
parsed.response !== null &&
|
||||
parsed.response.usage &&
|
||||
typeof parsed.response.usage === 'object'
|
||||
? parsed.response.usage
|
||||
: null;
|
||||
|
||||
if (!usage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const input = normalizeCodexUsageMetricValue(usage.input_tokens);
|
||||
const output = normalizeCodexUsageMetricValue(usage.output_tokens);
|
||||
const cached = normalizeCodexUsageMetricValue(usage.cached_input_tokens);
|
||||
const reasoning = normalizeCodexUsageMetricValue(usage.reasoning_output_tokens ?? usage.reasoning_tokens);
|
||||
const total =
|
||||
normalizeCodexUsageMetricValue(usage.total_tokens) ??
|
||||
normalizeCodexUsageMetricValue(usage.totalTokens) ??
|
||||
[input ?? 0, output ?? 0].reduce((sum, value) => sum + value, 0);
|
||||
|
||||
if (input === null && output === null && cached === null && reasoning === null && total === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
tokenTotals: {
|
||||
total: total ?? 0,
|
||||
input: input ?? 0,
|
||||
output: output ?? 0,
|
||||
cached: cached ?? 0,
|
||||
reasoning: reasoning ?? 0,
|
||||
},
|
||||
totalTokens: total ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
function extractCodexUsageSnapshotFromText(output) {
|
||||
const text = String(output ?? '');
|
||||
|
||||
if (!text.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lines = text
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
||||
const line = lines[index] ?? '';
|
||||
|
||||
if (!line.startsWith('{')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const usageSnapshot = extractCodexUsageSnapshot(JSON.parse(line));
|
||||
|
||||
if (usageSnapshot) {
|
||||
return usageSnapshot;
|
||||
}
|
||||
} catch {
|
||||
// ignore malformed JSON lines during fallback scan
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function finalizeCodexExecution(record) {
|
||||
if (record.completed) {
|
||||
return;
|
||||
@@ -642,6 +740,7 @@ async function runCodexLiveExecution(payload, response) {
|
||||
const resourceDir = path.join(repoPath, 'public', '.codex_chat', sessionId, 'resource');
|
||||
const uploadDir = path.join(resourceDir, 'uploads');
|
||||
const codexBin = process.env.SERVER_COMMAND_RUNNER_CODEX_BIN?.trim() || process.env.PLAN_CODEX_BIN?.trim() || 'codex';
|
||||
const codexModel = resolveCodexLiveModel(payload?.model);
|
||||
const configuredIdleTimeoutMs = resolveCodexLiveIdleTimeoutMs(payload?.idleTimeoutSeconds);
|
||||
const configuredMaxExecutionMs = resolveCodexLiveMaxExecutionMs(payload?.maxExecutionSeconds, configuredIdleTimeoutMs);
|
||||
|
||||
@@ -673,13 +772,14 @@ async function runCodexLiveExecution(payload, response) {
|
||||
let stderrTail = '';
|
||||
let jsonLineBuffer = '';
|
||||
let completedText = '';
|
||||
let streamedUsageSnapshot = null;
|
||||
let idleTimer = null;
|
||||
let executionTimer = null;
|
||||
let terminationRequested = false;
|
||||
|
||||
const child = spawn(
|
||||
codexBin,
|
||||
['exec', '--model', CODEX_LIVE_MODEL, '--json', '--dangerously-bypass-approvals-and-sandbox', '-C', repoPath, '-'],
|
||||
['exec', '--model', codexModel, '--json', '--dangerously-bypass-approvals-and-sandbox', '-C', repoPath, '-'],
|
||||
{
|
||||
cwd: repoPath,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
@@ -703,6 +803,7 @@ async function runCodexLiveExecution(payload, response) {
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'started',
|
||||
pid: child.pid ?? null,
|
||||
model: codexModel,
|
||||
configuredIdleTimeoutSeconds: Math.round(configuredIdleTimeoutMs / 1000),
|
||||
configuredMaxExecutionSeconds: Math.round(configuredMaxExecutionMs / 1000),
|
||||
});
|
||||
@@ -785,16 +886,7 @@ async function runCodexLiveExecution(payload, response) {
|
||||
}
|
||||
|
||||
const { completedText: nextCompletedText, deltaText } = extractCodexStreamText(parsed);
|
||||
|
||||
if (nextCompletedText) {
|
||||
refreshIdleTimer();
|
||||
completedText = nextCompletedText;
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'completed',
|
||||
text: nextCompletedText,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
const usageSnapshot = extractCodexUsageSnapshot(parsed);
|
||||
|
||||
if (deltaText) {
|
||||
refreshIdleTimer();
|
||||
@@ -802,10 +894,28 @@ async function runCodexLiveExecution(payload, response) {
|
||||
type: 'delta',
|
||||
text: deltaText,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
if (usageSnapshot) {
|
||||
streamedUsageSnapshot = usageSnapshot;
|
||||
refreshIdleTimer();
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'usage',
|
||||
usageSnapshot,
|
||||
});
|
||||
}
|
||||
|
||||
if (nextCompletedText) {
|
||||
refreshIdleTimer();
|
||||
completedText = nextCompletedText;
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'completed',
|
||||
text: nextCompletedText,
|
||||
usageSnapshot,
|
||||
});
|
||||
}
|
||||
|
||||
return Boolean(deltaText || usageSnapshot || nextCompletedText || activityLog);
|
||||
};
|
||||
|
||||
child.stdout?.on('data', (chunk) => {
|
||||
@@ -876,6 +986,18 @@ async function runCodexLiveExecution(payload, response) {
|
||||
handleCodexJsonLine(trailingLine);
|
||||
}
|
||||
|
||||
if (!streamedUsageSnapshot) {
|
||||
const fallbackUsageSnapshot = extractCodexUsageSnapshotFromText([stdoutTail, trailingLine].filter(Boolean).join('\n'));
|
||||
|
||||
if (fallbackUsageSnapshot) {
|
||||
streamedUsageSnapshot = fallbackUsageSnapshot;
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'usage',
|
||||
usageSnapshot: fallbackUsageSnapshot,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (code !== 0) {
|
||||
broadcastCodexExecutionEvent(executionRecord, {
|
||||
type: 'error',
|
||||
|
||||
Reference in New Issue
Block a user