Files
cc-web/scripts/mock-codex-app-server.js
2026-07-24 08:00:31 +08:00

1183 lines
32 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
const crypto = require('crypto');
const path = require('path');
const readline = require('readline');
const { spawn } = require('child_process');
const args = process.argv.slice(2);
if (args[0] !== 'app-server') {
const child = spawn(process.execPath, [path.join(__dirname, 'mock-codex.js'), ...args], {
stdio: 'inherit',
});
child.on('exit', (code, signal) => {
if (signal) process.kill(process.pid, signal);
process.exit(code || 0);
});
child.on('error', (err) => {
console.error(err.stack || err.message);
process.exit(1);
});
return;
}
const threads = new Map();
const pendingServerRequests = new Map();
const resumeMismatchThreads = new Set();
let nextServerRequestId = 1;
let mcpReloadCount = 0;
function send(message) {
process.stdout.write(`${JSON.stringify(message)}\n`);
}
function textFromInput(input) {
return (Array.isArray(input) ? input : [])
.map((item) => {
if (item?.type === 'text') return item.text || '';
if (item?.type === 'localImage') return `[image:${path.basename(item.path || 'image')}]`;
return '';
})
.filter(Boolean)
.join('\n')
.trim();
}
function tokenUsage(text) {
const inputTokens = Math.max(1, Math.ceil(String(text || '').length / 4));
const outputTokens = 7;
return {
last: {
inputTokens,
cachedInputTokens: 1,
outputTokens,
reasoningOutputTokens: 0,
totalTokens: inputTokens + outputTokens + 1,
},
total: {
inputTokens,
cachedInputTokens: 1,
outputTokens,
reasoningOutputTokens: 0,
totalTokens: inputTokens + outputTokens + 1,
},
};
}
function retryScenarioKey(text, marker) {
return new RegExp(marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i').test(String(text || ''))
? marker
: String(text || '');
}
function collaborationSummary(params = {}) {
const collaborationMode = params.collaborationMode;
const settings = collaborationMode?.settings || {};
const developerInstructions = String(settings.developer_instructions || '');
return JSON.stringify({
mode: collaborationMode?.mode || null,
hasModel: Boolean(settings.model),
hasDeveloperInstructions: /Codex sub-agent runtime rules/.test(developerInstructions),
hasSchemaDrivenSubagents: /current runtime tool schema/.test(developerInstructions),
hasLegacyV1Guidance: /fork_context|additional wait_agent rounds/.test(developerInstructions),
hasReasoningEffort: Object.prototype.hasOwnProperty.call(settings, 'reasoning_effort'),
reasoningEffort: settings.reasoning_effort || null,
hasTopLevelModel: Object.prototype.hasOwnProperty.call(params, 'model'),
hasTopLevelEffort: Object.prototype.hasOwnProperty.call(params, 'effort'),
});
}
function ensureThread(threadId, params = {}) {
const id = threadId || `app-thread-${crypto.randomUUID()}`;
if (!threads.has(id)) {
threads.set(id, {
id,
cwd: params.cwd || process.cwd(),
dynamicTools: Array.isArray(params.dynamicTools) ? params.dynamicTools : [],
config: params.config && typeof params.config === 'object' ? params.config : {},
activeTurnId: null,
timer: null,
steers: [],
capacityRetryAttempts: new Map(),
reconnectRetryAttempts: new Map(),
goal: null,
});
}
const thread = threads.get(id);
if (Array.isArray(params.dynamicTools)) thread.dynamicTools = params.dynamicTools;
if (params.config && typeof params.config === 'object') thread.config = params.config;
return thread;
}
function threadPayload(thread) {
return {
id: thread.id,
cwd: thread.cwd,
status: thread.activeTurnId ? 'running' : 'idle',
turns: [],
};
}
function emitChildCollabTurn(threadId, turnId, finalMessage) {
const childThread = ensureThread(threadId);
childThread.activeTurnId = turnId;
send({
method: 'turn/started',
params: {
threadId,
turn: {
id: turnId,
status: 'running',
items: [],
},
},
});
send({
method: 'item/agentMessage/delta',
params: {
threadId,
turnId,
itemId: 'agent-msg',
delta: finalMessage,
},
});
send({
method: 'item/completed',
params: {
threadId,
turnId,
completedAtMs: Date.now(),
item: {
id: 'agent-msg',
type: 'agentMessage',
content: [{ type: 'text', text: finalMessage }],
status: 'completed',
},
},
});
send({
method: 'turn/completed',
params: {
threadId,
turn: {
id: turnId,
status: 'completed',
items: [],
},
},
});
childThread.activeTurnId = null;
}
function emitNestedChildCollabTurn() {
const threadId = 'child-thread-v2';
const turnId = 'child-turn-v2';
const grandchildThreadId = 'grandchild-thread-v2';
const childThread = ensureThread(threadId);
childThread.activeTurnId = turnId;
send({
method: 'turn/started',
params: {
threadId,
turn: { id: turnId, status: 'running', items: [] },
},
});
send({
method: 'turn/plan/updated',
params: {
threadId,
turnId,
plan: [
{ step: '定位子线程路由', status: 'completed' },
{ step: '同步父卡片进度', status: 'in_progress' },
{ step: '完成集成回归', status: 'pending' },
],
},
});
send({
method: 'item/agentMessage/delta',
params: {
threadId,
turnId,
itemId: 'child-agent-msg-v2',
delta: 'V2 子代理最终消息:父级子代理路由正常。',
},
});
send({
method: 'item/completed',
params: {
threadId,
turnId,
completedAtMs: Date.now(),
item: {
id: 'grandchild-activity-v2',
type: 'subAgentActivity',
kind: 'started',
agentThreadId: grandchildThreadId,
agentPath: '/root/v2_parent/v2_grandchild',
prompt: '验证 Multi-agent V2 嵌套子代理路由。',
},
},
});
emitChildCollabTurn(
grandchildThreadId,
'grandchild-turn-v2',
'V2 孙代理最终消息:嵌套路由正常。'
);
send({
method: 'item/completed',
params: {
threadId,
turnId,
completedAtMs: Date.now(),
item: {
id: 'child-agent-msg-v2',
type: 'agentMessage',
content: [{ type: 'text', text: 'V2 子代理最终消息:父级子代理路由正常。' }],
status: 'completed',
},
},
});
send({
method: 'turn/completed',
params: {
threadId,
turn: { id: turnId, status: 'completed', items: [] },
},
});
childThread.activeTurnId = null;
}
function completeTurn(thread, turnId, text, status = 'completed') {
if (thread.activeTurnId !== turnId) return;
const suffix = thread.steers.length > 0 ? ` | steer: ${thread.steers.join(' | ')}` : '';
const responseText = `Codex App mock handled: ${text}${suffix}`;
send({
method: 'item/agentMessage/delta',
params: {
threadId: thread.id,
turnId,
itemId: 'agent-msg',
delta: responseText,
},
});
if (/tool/i.test(text)) {
send({
method: 'item/started',
params: {
threadId: thread.id,
turnId,
startedAtMs: Date.now(),
item: {
id: 'tool-cmd',
type: 'commandExecution',
command: '/bin/bash -lc echo codexapp',
status: 'inProgress',
},
},
});
send({
method: 'item/commandExecution/outputDelta',
params: {
threadId: thread.id,
turnId,
itemId: 'tool-cmd',
delta: 'codexapp\n',
},
});
send({
method: 'item/completed',
params: {
threadId: thread.id,
turnId,
completedAtMs: Date.now(),
item: {
id: 'tool-cmd',
type: 'commandExecution',
command: '/bin/bash -lc echo codexapp',
aggregatedOutput: 'codexapp\n',
exitCode: 0,
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 v2/i.test(text)) {
send({
method: 'item/completed',
params: {
threadId: thread.id,
turnId,
completedAtMs: Date.now(),
item: {
id: 'child-activity-v2',
type: 'subAgentActivity',
kind: 'started',
agentThreadId: 'child-thread-v2',
agentPath: '/root/v2_parent',
prompt: '验证 Multi-agent V2 子代理路由。',
},
},
});
emitNestedChildCollabTurn();
send({
method: 'item/started',
params: {
threadId: thread.id,
turnId,
startedAtMs: Date.now(),
item: {
id: 'wait-empty-v2',
type: 'collabAgentToolCall',
tool: 'wait_agent',
receiverThreadIds: [],
agentsStates: {},
status: 'inProgress',
},
},
});
send({
method: 'item/completed',
params: {
threadId: thread.id,
turnId,
completedAtMs: Date.now(),
item: {
id: 'wait-empty-v2',
type: 'collabAgentToolCall',
tool: 'wait_agent',
receiverThreadIds: [],
agentsStates: {},
status: 'completed',
},
},
});
} else if (/subagent/i.test(text)) {
send({
method: 'item/started',
params: {
threadId: thread.id,
turnId,
startedAtMs: Date.now(),
item: {
id: 'tool-collab',
type: 'collabAgentToolCall',
tool: 'spawn_agent',
prompt: '整理前端改动并回报状态',
receiverThreadIds: ['child-thread-a', 'child-thread-b'],
agentsStates: {
'child-thread-a': {
name: '实现代理',
role: 'frontend',
status: 'in_progress',
summary: '正在补充结构化渲染与样式',
},
'child-thread-b': {
name: '验证代理',
role: 'qa',
status: 'completed',
summary: '已完成基础事件链检查',
},
},
status: 'inProgress',
},
},
});
send({
method: 'item/completed',
params: {
threadId: thread.id,
turnId,
completedAtMs: Date.now(),
item: {
id: 'tool-collab',
type: 'collabAgentToolCall',
tool: 'spawn_agent',
prompt: '整理前端改动并回报状态',
receiverThreadIds: ['child-thread-a', 'child-thread-b'],
agentsStates: {
'child-thread-a': {
name: '实现代理',
role: 'frontend',
status: 'completed',
summary: '结构化渲染已完成',
},
'child-thread-b': {
name: '验证代理',
role: 'qa',
status: 'completed',
summary: '事件链与持久化检查通过',
},
},
status: 'completed',
},
},
});
emitChildCollabTurn(
'child-thread-a',
'child-turn-a',
'子代理最终消息:结构化渲染和中断按钮链路已完成。'
);
emitChildCollabTurn(
'child-thread-b',
'child-turn-b',
'子代理最终消息:事件路由、持久化和状态推送检查通过。'
);
}
send({
method: 'thread/tokenUsage/updated',
params: {
threadId: thread.id,
turnId,
tokenUsage: tokenUsage(`${text}${suffix}`),
},
});
send({
method: 'turn/completed',
params: {
threadId: thread.id,
turn: {
id: turnId,
status,
items: [],
},
},
});
thread.activeTurnId = null;
thread.timer = null;
thread.steers = [];
}
function completeTurnWithoutTerminalNotification(thread, turnId, text) {
if (thread.activeTurnId !== turnId) return;
const responseText = `Codex App stale turn output: ${text}`;
send({
method: 'item/agentMessage/delta',
params: {
threadId: thread.id,
turnId,
itemId: 'agent-msg',
delta: responseText,
},
});
send({
method: 'item/completed',
params: {
threadId: thread.id,
turnId,
completedAtMs: Date.now(),
item: {
id: 'agent-msg',
type: 'agentMessage',
content: [{ type: 'text', text: responseText }],
status: 'completed',
},
},
});
send({
method: 'thread/tokenUsage/updated',
params: {
threadId: thread.id,
turnId,
tokenUsage: tokenUsage(text),
},
});
// 模拟 app-server 已结束内部 turn但终态通知在传输中丢失。
thread.activeTurnId = null;
thread.timer = null;
thread.steers = [];
}
function completeGoalBackgroundTurn(thread, objective) {
const turnId = `goal-turn-${crypto.randomUUID()}`;
const text = `Goal background output: ${objective}`;
thread.activeTurnId = turnId;
send({
method: 'turn/started',
params: {
threadId: thread.id,
turn: { id: turnId, status: 'running', items: [] },
},
});
send({
method: 'item/agentMessage/delta',
params: {
threadId: thread.id,
turnId,
itemId: 'goal-agent-msg',
delta: text,
},
});
send({
method: 'item/completed',
params: {
threadId: thread.id,
turnId,
completedAtMs: Date.now(),
item: {
id: 'goal-agent-msg',
type: 'agentMessage',
text,
status: 'completed',
},
},
});
send({
method: 'thread/tokenUsage/updated',
params: {
threadId: thread.id,
turnId,
tokenUsage: tokenUsage(text),
},
});
send({
method: 'turn/completed',
params: {
threadId: thread.id,
turn: {
id: turnId,
status: 'completed',
items: [],
},
},
});
thread.activeTurnId = null;
}
function requestClient(method, params, callback) {
const id = `mock-server-request-${nextServerRequestId++}`;
pendingServerRequests.set(id, callback);
send({ id, method, params });
}
function completeDynamicToolTurn(thread, turnId, text) {
const callId = 'dynamic-ccweb-list';
send({
method: 'item/started',
params: {
threadId: thread.id,
turnId,
startedAtMs: Date.now(),
item: {
id: callId,
type: 'dynamicToolCall',
namespace: 'ccweb',
tool: 'ccweb_list_conversations',
arguments: { limit: 5 },
status: 'inProgress',
},
},
});
requestClient('item/tool/call', {
threadId: thread.id,
turnId,
callId,
namespace: 'ccweb',
tool: 'ccweb_list_conversations',
arguments: { limit: 5 },
}, (message) => {
const result = message.result || {};
send({
method: 'item/completed',
params: {
threadId: thread.id,
turnId,
completedAtMs: Date.now(),
item: {
id: callId,
type: 'dynamicToolCall',
namespace: 'ccweb',
tool: 'ccweb_list_conversations',
arguments: { limit: 5 },
status: result.success === false ? 'failed' : 'completed',
contentItems: result.contentItems || [],
success: result.success !== false,
durationMs: 1,
},
},
});
completeTurn(thread, turnId, `dynamic result: ${JSON.stringify(result)}`);
});
}
function completeMcpToolTurn(thread, turnId) {
const itemId = 'mcp-ccweb-list';
const ccwebConfig = thread.config?.['mcp_servers.ccweb'] || null;
const projectConfig = thread.config?.['mcp_servers.reg-app-project'] || null;
const env = ccwebConfig?.env || {};
let urlSourceSessionId = null;
let urlSourceHopCount = null;
try {
if (ccwebConfig?.url) {
const parsedUrl = new URL(ccwebConfig.url);
urlSourceSessionId = parsedUrl.searchParams.get('sourceSessionId') || null;
urlSourceHopCount = parsedUrl.searchParams.get('sourceHopCount') || null;
}
} catch {}
const payload = {
ok: true,
currentConversationId: env.CC_WEB_SOURCE_SESSION_ID || urlSourceSessionId,
sourceHopCount: env.CC_WEB_CROSS_HOP_COUNT || urlSourceHopCount,
hasCcwebMcpConfig: Boolean(ccwebConfig),
hasProjectMcpConfig: Boolean(projectConfig),
ccwebType: ccwebConfig?.type || (ccwebConfig?.url ? 'streamable_http' : (ccwebConfig?.command ? 'stdio' : null)),
ccwebUrl: ccwebConfig?.url || null,
ccwebBearerTokenEnvVar: ccwebConfig?.bearer_token_env_var || null,
ccwebCommand: ccwebConfig?.command || null,
ccwebArgs: ccwebConfig?.args || null,
};
const itemBase = {
id: itemId,
type: 'mcpToolCall',
server: 'ccweb',
tool: 'ccweb_list_conversations',
arguments: { limit: 5 },
};
send({
method: 'item/started',
params: {
threadId: thread.id,
turnId,
startedAtMs: Date.now(),
item: {
...itemBase,
status: 'inProgress',
},
},
});
send({
method: 'item/completed',
params: {
threadId: thread.id,
turnId,
completedAtMs: Date.now(),
item: {
...itemBase,
status: 'completed',
result: {
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
structuredContent: payload,
isError: false,
},
},
},
});
completeTurn(thread, turnId, `mcp result: ${JSON.stringify(payload)}`);
}
function emitCapacityError(thread, turnId) {
send({
method: 'error',
params: {
threadId: thread.id,
turnId,
type: 'error',
error: {
type: 'service_unavailable_error',
code: 'server_is_overloaded',
message: 'Our servers are currently overloaded. Please try again later.',
param: null,
},
sequence_number: 2,
},
});
thread.activeTurnId = null;
}
function emitPartialCapacityOutput(thread, turnId) {
send({
method: 'item/agentMessage/delta',
params: {
threadId: thread.id,
turnId,
itemId: 'agent-msg',
delta: 'partial capacity output before retry',
},
});
send({
method: 'item/started',
params: {
threadId: thread.id,
turnId,
startedAtMs: Date.now(),
item: {
id: 'capacity-tool',
type: 'commandExecution',
command: '/bin/bash -lc echo capacity',
status: 'inProgress',
},
},
});
send({
method: 'item/commandExecution/outputDelta',
params: {
threadId: thread.id,
turnId,
itemId: 'capacity-tool',
delta: 'capacity tool output\n',
},
});
}
function completeGuidedInputTurn(thread, turnId) {
requestClient('item/tool/requestUserInput', {
threadId: thread.id,
turnId,
itemId: 'guided-input-call',
questions: [{
id: 'choice',
header: '选择',
question: '选择一个测试答案',
isOther: true,
isSecret: false,
options: [
{ label: 'A', description: '测试选项 A' },
{ label: 'B', description: '测试选项 B' },
],
}],
}, (message) => {
const answer = message.result?.answers?.choice?.answers?.[0] || 'empty';
completeTurn(thread, turnId, `guided answer: ${answer}`);
});
}
function completeApprovalTurn(thread, turnId) {
requestClient('item/commandExecution/requestApproval', {
threadId: thread.id,
turnId,
itemId: 'approval-command-call',
reason: 'Need to run an approval-gated command',
command: 'echo approved',
cwd: thread.cwd,
}, (message) => {
const decision = message.result?.decision || 'missing';
completeTurn(thread, turnId, `approval decision: ${decision}`);
});
}
function completeEmptyReasoningTurn(thread, turnId, text) {
send({
method: 'item/started',
params: {
threadId: thread.id,
turnId,
startedAtMs: Date.now(),
item: {
id: 'reasoning-empty',
type: 'reasoning',
content: [],
summary: [],
status: 'inProgress',
},
},
});
send({
method: 'item/completed',
params: {
threadId: thread.id,
turnId,
completedAtMs: Date.now(),
item: {
id: 'reasoning-empty',
type: 'reasoning',
content: [],
summary: [],
status: 'completed',
},
},
});
completeTurn(thread, turnId, text);
}
function startTurn(params) {
const thread = ensureThread(params.threadId, params);
const turnId = `app-turn-${crypto.randomUUID()}`;
const text = textFromInput(params.input);
thread.activeTurnId = turnId;
thread.steers = [];
send({
method: 'turn/started',
params: {
threadId: thread.id,
turn: {
id: turnId,
status: 'running',
items: [],
},
},
});
if (/^codexapp stale running first$/i.test(text)) {
completeTurnWithoutTerminalNotification(thread, turnId, text);
return { turn: { id: turnId, status: 'running', items: [] } };
}
if (/^codexapp expected turn mismatch first$/i.test(text)) {
send({
method: 'item/agentMessage/delta',
params: {
threadId: thread.id,
turnId,
itemId: 'agent-msg',
delta: `Codex App expected turn mismatch fixture: ${text}`,
},
});
// 保留一个不同的活动 turn确保 steer 返回“不匹配”而不是“无活动 turn”。
thread.activeTurnId = `app-turn-mismatch-${crypto.randomUUID()}`;
return { turn: { id: turnId, status: 'running', items: [] } };
}
if (/runtime warning/i.test(text)) {
const message = 'Heads up: Long threads and multiple compactions can cause the model to be less accurate. Start a new thread when possible to keep threads small and targeted.';
for (let i = 0; i < 2; i += 1) {
send({
method: 'warning',
params: {
threadId: thread.id,
turnId,
message,
},
});
}
}
if (/codexapp capacity retry/i.test(text)) {
const retryKey = retryScenarioKey(text, 'codexapp capacity retry');
const attempts = (thread.capacityRetryAttempts.get(retryKey) || 0) + 1;
thread.capacityRetryAttempts.set(retryKey, attempts);
if (attempts <= 2) {
if (attempts === 2) emitPartialCapacityOutput(thread, turnId);
emitCapacityError(thread, turnId);
return { turn: { id: turnId, status: 'running', items: [] } };
}
}
if (/codexapp reconnect retry/i.test(text)) {
const retryKey = retryScenarioKey(text, 'codexapp reconnect retry');
const attempts = (thread.reconnectRetryAttempts.get(retryKey) || 0) + 1;
thread.reconnectRetryAttempts.set(retryKey, attempts);
if (attempts === 1) {
emitPartialCapacityOutput(thread, turnId);
send({
method: 'error',
params: {
threadId: thread.id,
turnId,
message: 'Reconnecting... 1/5',
},
});
thread.activeTurnId = null;
return { turn: { id: turnId, status: 'running', items: [] } };
}
}
if (/codexapp retry thread mismatch/i.test(text)) {
const retryKey = retryScenarioKey(text, 'codexapp retry thread mismatch');
const attempts = (thread.capacityRetryAttempts.get(retryKey) || 0) + 1;
thread.capacityRetryAttempts.set(retryKey, attempts);
if (attempts === 1) {
resumeMismatchThreads.add(thread.id);
emitCapacityError(thread, turnId);
return { turn: { id: turnId, status: 'running', items: [] } };
}
}
if (/collaboration/i.test(text)) {
completeTurn(thread, turnId, `collaboration mode: ${collaborationSummary(params)}`);
return { turn: { id: turnId, status: 'running', items: [] } };
}
if (/empty reasoning/i.test(text)) {
completeEmptyReasoningTurn(thread, turnId, text);
return { turn: { id: turnId, status: 'running', items: [] } };
}
if (/dynamic/i.test(text)) {
completeMcpToolTurn(thread, turnId);
return { turn: { id: turnId, status: 'running', items: [] } };
}
if (/guided/i.test(text)) {
if (!params.collaborationMode || params.collaborationMode.mode !== 'plan') {
completeTurn(thread, turnId, 'guided input unavailable outside plan');
return { turn: { id: turnId, status: 'running', items: [] } };
}
completeGuidedInputTurn(thread, turnId);
return { turn: { id: turnId, status: 'running', items: [] } };
}
if (/approval/i.test(text)) {
completeApprovalTurn(thread, turnId);
return { turn: { id: turnId, status: 'running', items: [] } };
}
const delay = /recover/i.test(text) ? 5000 : /slow/i.test(text) ? 900 : 80;
if (/recover/i.test(text)) {
send({
method: 'item/agentMessage/delta',
params: {
threadId: thread.id,
turnId,
itemId: 'agent-msg',
delta: `partial before restart: ${text}`,
},
});
send({
method: 'item/started',
params: {
threadId: thread.id,
turnId,
startedAtMs: Date.now(),
item: {
id: 'recover-tool',
type: 'commandExecution',
command: '/bin/bash -lc echo recover',
status: 'inProgress',
},
},
});
send({
method: 'item/commandExecution/outputDelta',
params: {
threadId: thread.id,
turnId,
itemId: 'recover-tool',
delta: 'recover tool output\n',
},
});
}
thread.timer = setTimeout(() => completeTurn(thread, turnId, text), delay);
return { turn: { id: turnId, status: 'running', items: [] } };
}
function interruptTurn(params) {
const thread = ensureThread(params.threadId, params);
if (thread.timer) clearTimeout(thread.timer);
if (thread.activeTurnId === params.turnId) {
completeTurn(thread, params.turnId, 'interrupted', 'interrupted');
}
return {};
}
function steerTurn(params) {
const thread = ensureThread(params.threadId, params);
if (!thread.activeTurnId) {
return {
error: {
code: -32001,
message: 'no active turn to steer',
},
};
}
if (thread.activeTurnId !== params.expectedTurnId) {
return {
error: {
code: -32001,
message: 'expectedTurnId does not match active turn',
},
};
}
const text = textFromInput(params.input);
if (text) thread.steers.push(text);
send({
method: 'item/agentMessage/delta',
params: {
threadId: thread.id,
turnId: thread.activeTurnId,
itemId: 'agent-msg',
delta: `\n[steer accepted: ${text}]`,
},
});
return { result: {} };
}
function handleRequest(message) {
const id = message.id;
const method = message.method;
const params = message.params || {};
if (id && !method && pendingServerRequests.has(id)) {
const callback = pendingServerRequests.get(id);
pendingServerRequests.delete(id);
callback(message);
return;
}
try {
if (method === 'initialize') {
send({ id, result: { serverInfo: { name: 'mock-codex-app-server', version: '0.0.0' } } });
return;
}
if (method === 'experimentalFeature/enablement/set') {
send({ id, result: { enablement: params.enablement || {} } });
return;
}
if (method === 'collaborationMode/list') {
send({ id, result: { data: [{ mode: 'default' }, { mode: 'plan' }] } });
return;
}
if (method === 'config/mcpServer/reload') {
mcpReloadCount += 1;
send({
method: 'mcpServer/startupStatus/updated',
params: {
server: 'ccweb',
state: 'starting',
message: 'ccweb MCP starting',
threadId: null,
},
});
send({
method: 'mcpServer/startupStatus/updated',
params: {
name: 'ccweb',
status: 'ready',
message: 'ccweb MCP ready CC_WEB_MCP_TOKEN=mock-secret-token',
threadId: null,
},
});
send({ id, result: { reloaded: true, reloadCount: mcpReloadCount } });
return;
}
if (method === 'thread/start') {
const thread = ensureThread(null, params);
send({ id, result: { thread: threadPayload(thread), model: params.model || 'gpt-5.5', cwd: thread.cwd, modelProvider: 'mock', approvalPolicy: params.approvalPolicy || 'never', approvalsReviewer: 'user', sandbox: params.sandbox || 'danger-full-access' } });
return;
}
if (method === 'thread/resume') {
if (params.threadId && resumeMismatchThreads.delete(params.threadId)) {
const thread = ensureThread(null, params);
send({ id, result: { thread: threadPayload(thread), model: params.model || 'gpt-5.5', cwd: thread.cwd, modelProvider: 'mock', approvalPolicy: params.approvalPolicy || 'never', approvalsReviewer: 'user', sandbox: params.sandbox || 'danger-full-access' } });
return;
}
const thread = ensureThread(params.threadId, params);
send({ id, result: { thread: threadPayload(thread), model: params.model || 'gpt-5.5', cwd: thread.cwd, modelProvider: 'mock', approvalPolicy: params.approvalPolicy || 'never', approvalsReviewer: 'user', sandbox: params.sandbox || 'danger-full-access' } });
return;
}
if (method === 'thread/goal/get') {
const thread = ensureThread(params.threadId, params);
send({ id, result: { goal: thread.goal } });
return;
}
if (method === 'thread/goal/set') {
const thread = ensureThread(params.threadId, params);
const now = Date.now();
const previous = thread.goal || {};
const objective = String(params.objective || previous.objective || 'mock goal').trim();
thread.goal = {
threadId: thread.id,
objective,
status: String(params.status || previous.status || 'active'),
tokenBudget: previous.tokenBudget ?? null,
tokensUsed: previous.tokensUsed ?? 3,
timeUsedSeconds: previous.timeUsedSeconds ?? 0,
createdAt: previous.createdAt || now,
updatedAt: now,
};
send({
method: 'thread/goal/updated',
params: { threadId: thread.id, goal: thread.goal },
});
setTimeout(() => {
send({ id, result: { goal: thread.goal } });
if (params.objective) {
setTimeout(() => completeGoalBackgroundTurn(thread, objective), 50);
}
}, 250);
return;
}
if (method === 'thread/goal/clear') {
const thread = ensureThread(params.threadId, params);
const cleared = !!thread.goal;
thread.goal = null;
send({
method: 'thread/goal/cleared',
params: { threadId: thread.id },
});
send({ id, result: { cleared } });
return;
}
if (method === 'turn/start') {
send({ id, result: startTurn(params) });
return;
}
if (method === 'turn/steer') {
const result = steerTurn(params);
send({ id, ...result });
return;
}
if (method === 'turn/interrupt') {
send({ id, result: interruptTurn(params) });
return;
}
if (method === 'initialized') return;
send({ id, error: { code: -32601, message: `Unknown mock method: ${method}` } });
} catch (err) {
send({ id, error: { code: -32603, message: err.message || String(err) } });
}
}
const rl = readline.createInterface({ input: process.stdin });
rl.on('line', (line) => {
if (!line.trim()) return;
let message;
try {
message = JSON.parse(line);
} catch {
return;
}
if (Object.prototype.hasOwnProperty.call(message, 'id')) handleRequest(message);
});