Stabilize ccweb codex app runtime

This commit is contained in:
shiyue
2026-06-16 09:09:23 +08:00
parent 0f4a1c27fe
commit 2e119fd7e3
6 changed files with 1361 additions and 124 deletions

View File

@@ -162,6 +162,49 @@ function completeTurn(thread, turnId, text, status = 'completed') {
});
}
if (/huge output/i.test(text)) {
const hugeOutput = `huge-output-start\n${'0123456789abcdef'.repeat(30000)}\nhuge-output-end\n`;
send({
method: 'item/started',
params: {
threadId: thread.id,
turnId,
startedAtMs: Date.now(),
item: {
id: 'huge-tool',
type: 'commandExecution',
command: '/bin/bash -lc huge-output',
status: 'inProgress',
},
},
});
send({
method: 'item/commandExecution/outputDelta',
params: {
threadId: thread.id,
turnId,
itemId: 'huge-tool',
delta: hugeOutput,
},
});
send({
method: 'item/completed',
params: {
threadId: thread.id,
turnId,
completedAtMs: Date.now(),
item: {
id: 'huge-tool',
type: 'commandExecution',
command: '/bin/bash -lc huge-output',
aggregatedOutput: hugeOutput,
exitCode: 0,
status: 'completed',
},
},
});
}
if (/subagent|collab/i.test(text)) {
send({
method: 'item/started',

View File

@@ -859,6 +859,19 @@ async function main() {
assert(storedCodexApp.messages.some((message) => message.role === 'assistant' && /codexapp tool prompt/.test(String(message.content || ''))), 'Codex App assistant response should be persisted');
assert((storedCodexApp.totalUsage?.inputTokens || 0) > 0, 'Codex App token usage should be persisted');
ws.send(JSON.stringify({ type: 'message', text: 'codexapp huge output prompt', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
const codexAppHugeTool = await nextMessage(messages, ws, (msg) => msg.type === 'tool_end' && msg.sessionId === codexAppSession.sessionId && msg.toolUseId === 'huge-tool');
assert((codexAppHugeTool.result || '').length <= 33000, 'Codex App huge tool result should be capped before sending to the browser');
assert(/内容过长|huge-output-start/.test(codexAppHugeTool.result || ''), 'Codex App huge tool result should keep a clear truncated preview');
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
storedCodexApp = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexAppSession.sessionId}.json`), 'utf8'));
const persistedHugeTool = storedCodexApp.messages
.flatMap((message) => Array.isArray(message.toolCalls) ? message.toolCalls : [])
.find((tool) => tool.id === 'huge-tool');
assert(persistedHugeTool, 'Codex App huge tool call should be persisted as a preview');
assert(String(persistedHugeTool.result || '').length <= 33000, 'Persisted Codex App huge tool result should be capped');
assert(fs.statSync(path.join(sessionsDir, `${codexAppSession.sessionId}.json`)).size < 1024 * 1024, 'Codex App huge output should not inflate session JSON beyond 1MB');
const reloadMcpResult = await postAuthedJson(port, token, `/api/sessions/${codexAppSession.sessionId}/reload-mcp`);
assert(reloadMcpResult.sessionId === codexAppSession.sessionId, 'Codex App MCP reload should return the target session id');
assert(reloadMcpResult.result?.reloaded === true, 'Codex App MCP reload should call app-server config/mcpServer/reload');
@@ -1098,6 +1111,56 @@ async function main() {
} finally {
await restartedRecoveryServer.stop();
}
const oversizedRecoverySessionId = 'oversized-recovery-session';
const oversizedRecoveryStateDir = path.join(sessionsDir, `${oversizedRecoverySessionId}-run`);
const oversizedRecoveryStatePath = path.join(oversizedRecoveryStateDir, 'codexapp-state.json');
fs.writeFileSync(path.join(sessionsDir, `${oversizedRecoverySessionId}.json`), JSON.stringify({
id: oversizedRecoverySessionId,
title: 'Oversized Recovery',
created: new Date().toISOString(),
updated: new Date().toISOString(),
pinnedAt: null,
agent: 'codexapp',
claudeSessionId: null,
codexThreadId: null,
codexAppThreadId: 'oversized-thread',
model: 'gpt-5.5(xhigh)',
permissionMode: 'yolo',
totalCost: 0,
totalUsage: { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0 },
messages: [],
cwd: homeDir,
}, null, 2));
mkdirp(oversizedRecoveryStateDir);
fs.writeFileSync(oversizedRecoveryStatePath, JSON.stringify({
version: 1,
agent: 'codexapp',
sessionId: oversizedRecoverySessionId,
threadId: 'oversized-thread',
turnId: 'oversized-turn',
turnStatus: 'running',
fullText: 'x'.repeat(5 * 1024 * 1024),
toolCalls: [],
}));
assert(fs.statSync(oversizedRecoveryStatePath).size > 4 * 1024 * 1024, 'Oversized recovery fixture should exceed the state load guard');
const oversizedRecoveryServer = await startServer(recoveryEnv);
try {
const { ws, messages } = await connectWs(recoveryPort, password);
await nextMessage(messages, ws, (msg) => msg.type === 'session_list' && msg.sessions.some((session) => session.id === oversizedRecoverySessionId));
assert(!fs.existsSync(oversizedRecoveryStateDir), 'Oversized Codex App recovery state directory should be cleaned without parsing the state');
ws.send(JSON.stringify({ type: 'load_session', sessionId: oversizedRecoverySessionId }));
const oversizedSessionInfo = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.sessionId === oversizedRecoverySessionId);
assert((oversizedSessionInfo.messages || []).some((message) => (
message.role === 'system' &&
/状态文件异常/.test(String(message.content || '')) &&
/跳过恢复/.test(String(message.content || ''))
)), 'Oversized Codex App recovery should add a system notice');
ws.close();
} finally {
await oversizedRecoveryServer.stop();
}
}
main().catch((err) => {