chore: rebuild release package

This commit is contained in:
shiyue
2026-07-24 08:00:31 +08:00
parent e5059e97c4
commit 4ab02ae33b
15 changed files with 778 additions and 73 deletions

View File

@@ -74,12 +74,15 @@ function retryScenarioKey(text, marker) {
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 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 || '')),
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'),
});
@@ -167,6 +170,85 @@ function emitChildCollabTurn(threadId, turnId, finalMessage) {
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(' | ')}` : '';
@@ -267,7 +349,57 @@ function completeTurn(thread, turnId, text, status = 'completed') {
});
}
if (/subagent/i.test(text)) {
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: {
@@ -331,7 +463,7 @@ function completeTurn(thread, turnId, text, status = 'completed') {
emitChildCollabTurn(
'child-thread-a',
'child-turn-a',
'子代理最终消息:结构化渲染和关闭按钮链路已完成。'
'子代理最终消息:结构化渲染和中断按钮链路已完成。'
);
emitChildCollabTurn(
'child-thread-b',

View File

@@ -882,12 +882,12 @@ function assertPlanListProgressContract() {
assert(result.progress?.completed === 3 && result.progress?.total === 5, 'Persisted todo result should expose 3/5 progress');
const summarySource = extractFunctionSource(source, 'applyToolSummary');
const progressElementSource = extractFunctionSource(source, 'createPlanProgressElement');
const progressElementSource = extractFunctionSource(source, 'createPlanProgressElementFromProgress');
assert(summarySource.includes('createPlanProgressElement(tool)'), 'Tool summaries should append the plan progress element beside the title');
assert(progressElementSource.includes("meter.setAttribute('role', 'img')"), 'Plan progress should expose an accessible image role');
assert(progressElementSource.includes("meter.setAttribute('aria-label', progressLabel)"), 'Plan progress should announce completed and total counts');
assert(progressElementSource.includes('Math.min(progress.total, 12)'), 'Long plans should cap visible dots to protect the header layout');
assert(progressElementSource.includes("count.textContent = `${progress.completed}/${progress.total}`"), 'Compacted long plans should keep an exact numeric count');
assert(progressElementSource.includes('Math.min(normalizedProgress.total, 12)'), 'Long plans should cap visible dots to protect the header layout');
assert(progressElementSource.includes("count.textContent = `${normalizedProgress.completed}/${normalizedProgress.total}`"), 'Compacted long plans should keep an exact numeric count');
assert(styleSource.includes('--plan-progress-complete: var(--success);'), 'Plan progress should inherit the active theme success color');
assert(styleSource.includes('--plan-progress-remaining: var(--accent);'), 'Remaining plan progress should inherit the active theme accent color');
@@ -1508,9 +1508,15 @@ function assertFrontendSubagentCardMetadataContract() {
assert(/mergeCollabAgentTaskState\(\s*states\[id\]/.test(source), 'Receiver-only child states should use the tested metadata merge helper');
assert(source.includes('entry.detail ? `结果: ${entry.detail}` :'), 'Card title should keep runtime result in the container title');
assert(source.includes("description.className = 'collab-agent-item-description'"), 'Sub-agent cards should render a visible task intro node');
assert(source.includes('function createPlanProgressElementFromProgress(progress'), 'Frontend should expose reusable plan progress rendering for sub-agent cards');
assert(source.includes("plan.className = 'collab-agent-item-plan'"), 'Sub-agent cards should render a compact plan progress row');
assert(source.includes("currentStep.className = 'collab-agent-item-plan-current'"), 'Sub-agent cards should render the current plan step');
assert(source.includes('description.title = descriptionTitle'), 'Task intro node should expose the full task intro or fallback in its title attribute');
assert(source.includes('label.textContent = displayTitle;'), 'Rendered card label should use normalized title selection');
assert(/\.collab-agent-item-description\s*\{[\s\S]*?-webkit-line-clamp:\s*2;/.test(styleSource), 'Sub-agent task intro should use two-line truncation');
assert(/\.collab-agent-item-plan\s*\{[\s\S]*?display:\s*flex;/.test(styleSource), 'Sub-agent plan progress should use a compact flex row');
assert(/\.collab-agent-item-plan \.plan-progress-dot\s*\{[\s\S]*?width:\s*8px;[\s\S]*?height:\s*8px;[\s\S]*?margin-left:\s*0;/.test(styleSource), 'Sub-agent plan dots should remain compact inside narrow cards');
assert(/\.collab-agent-item-plan-current\s*\{[\s\S]*?text-overflow:\s*ellipsis;/.test(styleSource), 'Sub-agent current plan step should truncate safely');
assert(/\.collab-agent-item\s*\{[\s\S]*?min-width:\s*0;[\s\S]*?flex-direction:\s*column;/.test(styleSource), 'Sub-agent cards should be vertically composed and flex-shrink on narrow screens');
assert(/@media \(max-width:\s*640px\)[\s\S]*?\.collab-agent-item\s*\{[\s\S]*?min-width:\s*0;[\s\S]*?\}/.test(styleSource), 'Narrow screens should let sub-agent cards shrink without horizontal overflow');
@@ -1666,6 +1672,33 @@ function assertFrontendSubagentCardMetadataContract() {
assert(spawnedMerge.input.receiverThreadIds.length === 1, 'Spawned child should merge into one visible agent');
assert(collabApi.getCachedState('child-thread-a')?.label === '关闭验证代理', 'Structured child state should be cached by thread id');
const planCardTool = {
...spawnedTool,
id: 'tool-plan-progress',
input: {
...spawnedTool.input,
receiverThreadIds: ['child-thread-plan'],
agentsStates: {
'child-thread-plan': {
title: '进度验证代理',
taskDescription: '验证子代理计划简报。',
status: 'running',
planProgress: { completed: 1, total: 3 },
planCurrentStep: '同步父卡片进度',
},
},
},
};
const planCardElement = collabApi.renderCollabAgentToolElement(planCardTool);
const planRows = findNodesByClass(planCardElement, 'collab-agent-item-plan');
const planDots = findNodesByClass(planCardElement, 'plan-progress-dot');
const planCounts = findNodesByClass(planCardElement, 'plan-progress-count');
const planCurrentSteps = findNodesByClass(planCardElement, 'collab-agent-item-plan-current');
assert(planRows.length === 1, 'Sub-agent plan summary should render one compact progress row');
assert(planDots.length === 3, 'Sub-agent plan summary should render one progress dot per short plan item');
assert(planCounts.length === 1 && planCounts[0].textContent === '1/3', 'Sub-agent plan summary should always render completed/total');
assert(planCurrentSteps.length === 1 && planCurrentSteps[0].textContent === '当前:同步父卡片进度', 'Sub-agent plan summary should render the current in-progress step');
collabApi.rememberCollabAgentState(
'unrelated-child',
{ title: '无关历史代理', status: 'running' },
@@ -1974,6 +2007,20 @@ function assertCodexAppRuntimeSubAgentActivityContract() {
fullText: '',
};
assert(typeof runtime.planUpdateFromNotification === 'function', 'Runtime should expose shared plan notification parsing');
const childPlan = runtime.planUpdateFromNotification({
method: 'turn/plan/updated',
params: {
plan: [
{ step: '解析子代理计划', status: 'completed' },
{ step: '同步父卡片进度', status: 'in_progress' },
{ step: '补充回归验证', status: 'pending' },
],
},
});
assert(childPlan?.progress?.completed === 1 && childPlan?.progress?.total === 3, 'Shared plan parser should summarize child plan progress');
assert(childPlan?.currentStep === '同步父卡片进度', 'Shared plan parser should expose the current in-progress step');
runtime.processCodexAppNotification(entry, {
method: 'item/started',
params: {
@@ -3423,6 +3470,47 @@ function assertCodexAppUnroutedNotificationRoutingContract() {
);
}
function assertMultiAgentV2CompatibilityContract() {
const serverSource = fs.readFileSync(SERVER_PATH, 'utf8');
const runtimeSource = fs.readFileSync(path.join(REPO_DIR, 'lib', 'agent-runtime.js'), 'utf8');
const frontendSource = fs.readFileSync(PUBLIC_APP_PATH, 'utf8');
assert(
/CODEX_REASONING_LEVELS\s*=\s*new Set\(\[[^\]]*'ultra'/.test(serverSource),
'Codex config model parsing should accept the ultra reasoning level'
);
assert(
/low\|medium\|high\|xhigh\|ultra/.test(runtimeSource),
'Codex CLI model suffix parsing should accept ultra'
);
assert(
frontendSource.includes("{ value: 'ultra', label: 'ultra'"),
'Codex model picker should expose ultra reasoning'
);
const instructionsBlock = serverSource.slice(
serverSource.indexOf('const CODEX_APP_COLLABORATION_INSTRUCTIONS'),
serverSource.indexOf('function getLocalCodexConfigTomlPath')
);
assert(instructionsBlock.includes('current runtime tool schema'), 'Sub-agent guidance should defer to the current runtime tool schema');
assert(!instructionsBlock.includes('fork_context'), 'Sub-agent guidance should not hard-code the V1 fork_context field');
assert(!instructionsBlock.includes('additional wait_agent rounds'), 'Sub-agent guidance should not impose obsolete repeated wait_agent calls');
const syncBlock = extractFunctionSource(serverSource, 'syncCcwebMcpChildAgentsFromCollabItem');
assert(syncBlock.includes("itemType === 'subAgentActivity'"), 'Child routing should register canonical V2 subAgentActivity items');
const notificationBlock = extractFunctionSource(serverSource, 'handleCodexAppNotification');
const activitySyncIndex = notificationBlock.indexOf('syncCcwebMcpChildAgentsFromCollabItem(routed, item)');
const childReturnIndex = notificationBlock.indexOf("if (routed.role === 'child')");
assert(activitySyncIndex >= 0 && activitySyncIndex < childReturnIndex, 'Nested subAgentActivity registration should happen before child notification routing returns');
const childNotificationBlock = extractFunctionSource(serverSource, 'processCcwebMcpChildNotification');
assert(childNotificationBlock.includes('codexAppRuntime.planUpdateFromNotification(notification)'), 'Child notification routing should reuse runtime plan parsing');
const recoveryBlock = extractFunctionSource(serverSource, 'recoverCcwebMcpChildThreadsFromPersistedToolCalls');
assert(recoveryBlock.includes('parseMaybeJsonObject(tool?.result)'), 'Child recovery should read the latest persisted collaboration tool result');
assert(recoveryBlock.includes('recoveredState.planProgress'), 'Child recovery should restore persisted plan progress');
assert(frontendSource.includes("closeBtn.textContent = '中断';"), 'Sub-agent action should use interrupt semantics');
assert(frontendSource.includes('entry.agentPath ? `路径: ${entry.agentPath}`'), 'Sub-agent cards should expose the canonical agent path');
}
async function main() {
const targetIndex = process.argv.indexOf('--target');
const regressionTarget = targetIndex >= 0 ? String(process.argv[targetIndex + 1] || '').trim() : String(process.env.CC_WEB_REGRESSION_TARGET || '').trim();
@@ -3440,6 +3528,7 @@ async function main() {
if (regressionTarget === 'subagent-card-metadata') {
assertFrontendSubagentCardMetadataContract();
assertCodexAppRuntimeSubAgentActivityContract();
assertMultiAgentV2CompatibilityContract();
console.log('Subagent card metadata regression checks passed.');
return;
}
@@ -3503,6 +3592,7 @@ async function main() {
assertSessionSwitchResilienceContract();
assertSessionSwitchRaceContract();
assertCodexAppChildToolFallbackContract();
assertMultiAgentV2CompatibilityContract();
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-web-regression-'));
const configDir = path.join(tempRoot, 'config');
@@ -3580,7 +3670,7 @@ async function main() {
}, null, 2));
createFakeClaudeHistory(homeDir);
createFakeCodexConfig(homeDir);
createFakeCodexConfig(homeDir, { reasoningEffort: 'ultra' });
const codexFixture = createFakeCodexHistory(homeDir);
const codexAppImportFixture = createFakeCodexHistory(homeDir, {
threadId: 'codexapp-import-thread',
@@ -3730,7 +3820,7 @@ async function main() {
ws.send(JSON.stringify({ type: 'new_session', agent: 'codex', cwd: codexInitCwd, mode: 'plan' }));
const codexSession = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.agent === 'codex' && msg.cwd === codexInitCwd);
assert(codexSession.mode === 'plan', 'Codex new_session should follow requested mode');
assert(codexSession.model === 'gpt-5.5(xhigh)', 'Codex new_session should read default model from ~/.codex/config.toml');
assert(codexSession.model === 'gpt-5.5(ultra)', 'Codex new_session should preserve ultra from ~/.codex/config.toml');
ws.send(JSON.stringify({ type: 'set_session_pinned', sessionId: codexSession.sessionId, pinned: true }));
const pinnedAck = await nextMessage(messages, ws, (msg) => msg.type === 'session_pinned' && msg.sessionId === codexSession.sessionId);
@@ -4319,6 +4409,8 @@ async function main() {
.split('\n')
.find((line) => line.includes(`"event":"process_spawn"`) && line.includes(firstMessageSession.sessionId.slice(0, 8)));
assert(spawnLine && !spawnLine.includes('--search') && spawnLine.includes('--image'), 'Codex exec should attach images and not append unsupported --search flag');
const parsedSpawnLine = JSON.parse(spawnLine);
assert(parsedSpawnLine.args.includes('model_reasoning_effort="ultra"'), 'Codex exec should pass the ultra reasoning level through model_reasoning_effort');
const allSpawnsForSession = processLog
.trim()
@@ -4383,7 +4475,7 @@ async function main() {
].join('\n'));
ws.send(JSON.stringify({ type: 'new_session', agent: 'codexapp', cwd: codexAppCwd, mode: 'yolo' }));
const codexAppSession = await nextMessage(messages, ws, (msg) => msg.type === 'session_info' && msg.agent === 'codexapp' && msg.cwd === codexAppCwd);
assert(codexAppSession.model === 'gpt-5.5(xhigh)', 'Codex App new_session should read default Codex model');
assert(codexAppSession.model === 'gpt-5.5(ultra)', 'Codex App new_session should preserve the ultra default Codex model');
ws.send(JSON.stringify({ type: 'composer_suggestions', requestId: 'reg-codexapp-skill', trigger: '$', query: 'reg', sessionId: codexAppSession.sessionId, agent: 'codexapp' }));
const codexAppSkillComposer = await nextMessage(messages, ws, (msg) => msg.type === 'composer_suggestions' && msg.requestId === 'reg-codexapp-skill');
@@ -4397,7 +4489,9 @@ async function main() {
assert(/"mode":"default"/.test(codexAppDefaultCollab.text || ''), 'Codex App YOLO mode should pass default collaboration mode');
assert(/"hasModel":true/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration settings should include model');
assert(/"hasDeveloperInstructions":true/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration settings should include sub-agent developer instructions');
assert(/"hasWaitAgentRetryGuidance":true/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration settings should include wait_agent retry guidance');
assert(/"hasSchemaDrivenSubagents":true/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration settings should use runtime-schema-driven sub-agent guidance');
assert(/"hasLegacyV1Guidance":false/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration settings should omit legacy V1 fork/wait guidance');
assert(/"reasoningEffort":"ultra"/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration settings should pass ultra reasoning_effort');
assert(/"hasTopLevelModel":false/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration turn should not duplicate model at top level');
assert(/"hasTopLevelEffort":false/.test(codexAppDefaultCollab.text || ''), 'Codex App collaboration turn should not duplicate effort at top level');
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
@@ -4775,13 +4869,13 @@ async function main() {
assert(/finalMessage/.test(ccwebMcpChildReturned.tool?.result || ''), 'ccweb MCP child final message should be merged into the parent tool result');
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);
ws.send(JSON.stringify({ type: 'ccweb_mcp_child_agent_close', sessionId: codexAppSession.sessionId, threadId: 'child-thread-a' }));
const ccwebMcpChildClosed = await nextMessage(messages, ws, (msg) =>
const ccwebMcpChildInterrupted = await nextMessage(messages, ws, (msg) =>
msg.type === 'ccweb_mcp_child_agent_update' &&
msg.sessionId === codexAppSession.sessionId &&
msg.child?.threadId === 'child-thread-a' &&
msg.child?.status === 'closed'
msg.child?.status === 'interrupted'
);
assert(/"status": "closed"/.test(ccwebMcpChildClosed.tool?.result || ''), 'ccweb MCP child close should update the parent collab tool state');
assert(/"status": "interrupted"/.test(ccwebMcpChildInterrupted.tool?.result || ''), 'ccweb MCP child interrupt should update the parent collab tool state');
storedCodexApp = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexAppSession.sessionId}.json`), 'utf8'));
const hasCollabTool = storedCodexApp.messages
.flatMap((message) => Array.isArray(message.toolCalls) ? message.toolCalls : [])
@@ -4791,13 +4885,76 @@ async function main() {
.flatMap((message) => Array.isArray(message.toolCalls) ? message.toolCalls : [])
.reverse()
.find((tool) => tool.id === 'tool-collab');
assert(/"status": "closed"/.test(persistedClosedCollabTool?.result || ''), 'ccweb MCP manual child close should persist closed state');
assert(/"status": "interrupted"/.test(persistedClosedCollabTool?.result || ''), 'ccweb MCP manual child interrupt should persist interrupted state');
ws.send(JSON.stringify({ type: 'message', text: 'codexapp subagent v2 prompt', sessionId: codexAppSession.sessionId, mode: 'yolo', agent: 'codexapp' }));
const codexAppV2ChildStarted = await nextMessage(messages, ws, (msg) =>
msg.type === 'ccweb_mcp_child_agent_update' &&
msg.sessionId === codexAppSession.sessionId &&
msg.child?.threadId === 'child-thread-v2' &&
msg.child?.status === 'running'
);
assert(codexAppV2ChildStarted.toolUseId === 'child-activity-v2', 'V2 child update should reference its subAgentActivity item');
assert(codexAppV2ChildStarted.child.agentPath === '/root/v2_parent', 'V2 child update should preserve its canonical agentPath');
const codexAppV2ChildPlan = await nextMessage(messages, ws, (msg) =>
msg.type === 'ccweb_mcp_child_agent_update' &&
msg.sessionId === codexAppSession.sessionId &&
msg.child?.threadId === 'child-thread-v2' &&
msg.child?.planProgress?.completed === 1 &&
msg.child?.planProgress?.total === 3
);
assert(codexAppV2ChildPlan.child.planCurrentStep === '同步父卡片进度', 'V2 child update should expose the current in-progress plan step');
const codexAppV2ChildPlanToolResult = JSON.parse(codexAppV2ChildPlan.tool?.result || '{}');
assert(
codexAppV2ChildPlanToolResult.agentsStates?.['child-thread-v2']?.planProgress?.completed === 1,
'V2 child plan progress should merge into the visible parent collaboration tool'
);
const codexAppV2GrandchildStarted = await nextMessage(messages, ws, (msg) =>
msg.type === 'ccweb_mcp_child_agent_update' &&
msg.sessionId === codexAppSession.sessionId &&
msg.child?.threadId === 'grandchild-thread-v2' &&
msg.child?.status === 'running'
);
assert(codexAppV2GrandchildStarted.child.parentThreadId === 'child-thread-v2', 'Nested V2 child should retain the immediate child as parentThreadId');
assert(codexAppV2GrandchildStarted.child.agentPath === '/root/v2_parent/v2_grandchild', 'Nested V2 child should preserve its canonical agentPath');
const codexAppV2GrandchildReturned = await nextMessage(messages, ws, (msg) =>
msg.type === 'ccweb_mcp_child_agent_update' &&
msg.sessionId === codexAppSession.sessionId &&
msg.child?.threadId === 'grandchild-thread-v2' &&
msg.child?.status === 'returned' &&
/V2 孙代理最终消息/.test(msg.child?.candidateResult || '')
);
assert(/grandchild-thread-v2/.test(codexAppV2GrandchildReturned.tool?.result || ''), 'Nested V2 child result should merge into a visible collaboration tool');
const codexAppV2ChildReturned = await nextMessage(messages, ws, (msg) =>
msg.type === 'ccweb_mcp_child_agent_update' &&
msg.sessionId === codexAppSession.sessionId &&
msg.child?.threadId === 'child-thread-v2' &&
msg.child?.status === 'returned' &&
/V2 子代理最终消息/.test(msg.child?.candidateResult || '')
);
assert(codexAppV2ChildReturned.child.parentThreadId, 'V2 child should retain the root parent thread id');
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 persistedV2ChildTool = storedCodexApp.messages
.flatMap((message) => Array.isArray(message.toolCalls) ? message.toolCalls : [])
.find((tool) => tool.id === 'child-activity-v2');
const persistedV2ChildResult = JSON.parse(persistedV2ChildTool?.result || '{}');
assert(
persistedV2ChildResult.agentsStates?.['child-thread-v2']?.planProgress?.completed === 1
&& persistedV2ChildResult.agentsStates?.['child-thread-v2']?.planProgress?.total === 3,
'V2 child plan progress should persist in the session collaboration tool for refresh recovery'
);
assert(
persistedV2ChildResult.agentsStates?.['child-thread-v2']?.planCurrentStep === '同步父卡片进度',
'V2 child current plan step should persist for refresh recovery'
);
ws.send(JSON.stringify({ type: 'message', text: 'codexapp collaboration plan probe', sessionId: codexAppSession.sessionId, mode: 'plan', agent: 'codexapp' }));
const codexAppPlanCollab = await nextMessage(messages, ws, (msg) => msg.type === 'text_delta' && msg.sessionId === codexAppSession.sessionId && /collaboration mode:/.test(msg.text || ''));
assert(/"mode":"plan"/.test(codexAppPlanCollab.text || ''), 'Codex App Plan mode should pass plan collaboration mode');
assert(/"hasDeveloperInstructions":true/.test(codexAppPlanCollab.text || ''), 'Codex App Plan collaboration settings should keep sub-agent developer instructions');
assert(/"hasWaitAgentRetryGuidance":true/.test(codexAppPlanCollab.text || ''), 'Codex App Plan collaboration settings should keep wait_agent retry guidance');
assert(/"hasSchemaDrivenSubagents":true/.test(codexAppPlanCollab.text || ''), 'Codex App Plan collaboration settings should keep runtime-schema-driven guidance');
assert(/"hasLegacyV1Guidance":false/.test(codexAppPlanCollab.text || ''), 'Codex App Plan collaboration settings should omit legacy V1 guidance');
assert(/"hasTopLevelModel":false/.test(codexAppPlanCollab.text || ''), 'Codex App Plan collaboration turn should not duplicate model at top level');
assert(/"hasTopLevelEffort":false/.test(codexAppPlanCollab.text || ''), 'Codex App Plan collaboration turn should not duplicate effort at top level');
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === codexAppSession.sessionId);