chore: sync backend and deployment changes

This commit is contained in:
2026-05-25 17:25:52 +09:00
parent d38d022872
commit fb5ec649cd
58 changed files with 17575 additions and 378 deletions

95
scripts/guard-staged-assets.mjs Executable file
View File

@@ -0,0 +1,95 @@
#!/usr/bin/env node
import { execFileSync } from 'node:child_process';
import { statSync } from 'node:fs';
import path from 'node:path';
const repoRoot = process.cwd();
const allowAssetCommit = process.env.ALLOW_ASSET_COMMIT === '1';
const maxBinarySizeBytes = 5 * 1024 * 1024;
const tmpCapturePattern = /^tmp-.*\.(png|jpe?g|webp|gif|mp4|mov|webm)$/i;
const binaryAssetPattern = /\.(png|jpe?g|webp|gif|svg|mp4|mov|webm|pdf|zip|7z)$/i;
const blockedAssetPrefixes = ['public/assets/'];
function getStagedPaths() {
const output = execFileSync(
'git',
['diff', '--cached', '--name-only', '--diff-filter=ACMR'],
{ cwd: repoRoot, encoding: 'utf8' }
);
return output
.split('\n')
.map((line) => line.trim())
.filter(Boolean);
}
function toDisplaySize(bytes) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
function isBlockedAssetPath(filePath) {
return blockedAssetPrefixes.some((prefix) => filePath.startsWith(prefix));
}
function main() {
const stagedPaths = getStagedPaths();
const violations = [];
for (const relativePath of stagedPaths) {
const absolutePath = path.join(repoRoot, relativePath);
let stats;
try {
stats = statSync(absolutePath);
} catch {
continue;
}
if (!stats.isFile()) continue;
const baseName = path.basename(relativePath);
const isTmpCapture = tmpCapturePattern.test(baseName);
const isBinaryAsset = binaryAssetPattern.test(relativePath);
const isOversizedBinary = isBinaryAsset && stats.size > maxBinarySizeBytes;
const isBlockedAsset = isBinaryAsset && isBlockedAssetPath(relativePath);
if (!isTmpCapture && !isOversizedBinary && !isBlockedAsset) continue;
const reasons = [];
if (isTmpCapture) reasons.push('temporary capture file');
if (isBlockedAsset) reasons.push('asset path under public/assets');
if (isOversizedBinary) {
reasons.push(`binary file exceeds ${toDisplaySize(maxBinarySizeBytes)}`);
}
violations.push({
relativePath,
size: stats.size,
reasons,
});
}
if (violations.length === 0 || allowAssetCommit) {
return;
}
console.error('');
console.error('Blocked commit: staged asset files need an explicit override.');
console.error('Set ALLOW_ASSET_COMMIT=1 only when the asset commit is intentional.');
console.error('');
for (const violation of violations) {
console.error(
`- ${violation.relativePath} (${toDisplaySize(violation.size)}): ${violation.reasons.join(', ')}`
);
}
console.error('');
process.exit(1);
}
main();

View File

@@ -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',

View File

@@ -25,6 +25,53 @@ const mimeTypes = {
'.woff2': 'font/woff2',
};
function canListenOnPort(candidatePort, host = '0.0.0.0') {
return new Promise((resolve, reject) => {
const probeServer = createServer();
probeServer.once('error', (error) => {
probeServer.close(() => {
if (error && typeof error === 'object' && 'code' in error && error.code === 'EADDRINUSE') {
resolve(false);
return;
}
reject(error);
});
});
probeServer.once('listening', () => {
probeServer.close((closeError) => {
if (closeError) {
reject(closeError);
return;
}
resolve(true);
});
});
probeServer.listen(candidatePort, host);
});
}
async function findAvailablePort(initialPort, host = '0.0.0.0', maxAttempts = 20) {
for (let offset = 0; offset < maxAttempts; offset += 1) {
const candidatePort = initialPort + offset;
const available = await canListenOnPort(candidatePort, host);
if (available) {
return candidatePort;
}
if (offset === 0) {
console.warn(`Port ${initialPort} is in use, trying another one...`);
}
}
throw new Error(`No available port found from ${initialPort} to ${initialPort + maxAttempts - 1}.`);
}
function resolveCacheControl(resolvedPath, extension) {
const normalizedPath = resolvedPath.replace(/\\/g, '/');
@@ -218,6 +265,9 @@ server.on('upgrade', (request, socket, head) => {
});
});
server.listen(port, '0.0.0.0', () => {
console.log(`${distDirName} server listening on http://0.0.0.0:${port}`);
const host = '0.0.0.0';
const resolvedPort = await findAvailablePort(port, host);
server.listen(resolvedPort, host, () => {
console.log(`${distDirName} server listening on http://${host}:${resolvedPort}`);
});