909 lines
25 KiB
JavaScript
Executable File
909 lines
25 KiB
JavaScript
Executable File
#!/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 || {};
|
|
return JSON.stringify({
|
|
mode: collaborationMode?.mode || null,
|
|
hasModel: Boolean(settings.model),
|
|
hasDeveloperInstructions: /Codex sub-agent spawning rules/.test(String(settings.developer_instructions || '')),
|
|
hasWaitAgentRetryGuidance: /wait_agent[\s\S]*timeout_ms[\s\S]*additional wait_agent rounds/.test(String(settings.developer_instructions || '')),
|
|
hasReasoningEffort: Object.prototype.hasOwnProperty.call(settings, 'reasoning_effort'),
|
|
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 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/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 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 || {};
|
|
const payload = {
|
|
ok: true,
|
|
currentConversationId: env.CC_WEB_SOURCE_SESSION_ID || null,
|
|
sourceHopCount: env.CC_WEB_CROSS_HOP_COUNT || null,
|
|
hasCcwebMcpConfig: Boolean(ccwebConfig),
|
|
hasProjectMcpConfig: Boolean(projectConfig),
|
|
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 (/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 || 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 },
|
|
});
|
|
send({ id, result: { goal: thread.goal } });
|
|
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);
|
|
});
|