Fix cross-conversation reply auto-resume
This commit is contained in:
@@ -286,6 +286,10 @@ function nextMessage(messages, ws, predicate, timeoutMs = 15000) {
|
||||
});
|
||||
}
|
||||
|
||||
function isSessionCompletionMessage(msg, sessionId) {
|
||||
return (msg?.type === 'done' || msg?.type === 'background_done') && msg.sessionId === sessionId;
|
||||
}
|
||||
|
||||
function createFakeClaudeHistory(homeDir) {
|
||||
const projectDir = path.join(homeDir, '.claude', 'projects', 'tmp-project');
|
||||
mkdirp(projectDir);
|
||||
@@ -854,6 +858,7 @@ async function main() {
|
||||
assert(mcpCreateReply.body.mode === 'yolo', 'MCP create requestReply should default to yolo when mode is omitted');
|
||||
assert(mcpCreateReply.body.cwd === mcpReplyCreateCwd, 'MCP create conversation should use an explicit absolute cwd');
|
||||
assert(mcpCreateReply.body.requestId && mcpCreateReply.body.replyStatus === 'waiting', 'MCP create requestReply should return a waiting request id');
|
||||
assert(mcpCreateReply.body.replyDelivery === 'auto_run' && mcpCreateReply.body.sourceAutoRun === true, 'MCP create requestReply should declare source auto-run delivery');
|
||||
await nextMessage(messages, ws, (msg) => msg.type === 'background_done' && msg.sessionId === mcpCreateReply.body.conversationId);
|
||||
await waitForJsonCondition(path.join(sessionsDir, `${codexSession.sessionId}.json`), (session) => (
|
||||
Array.isArray(session.messages) &&
|
||||
@@ -862,14 +867,19 @@ async function main() {
|
||||
message.crossConversation?.processed === true
|
||||
))
|
||||
));
|
||||
await nextMessage(messages, ws, (msg) => isSessionCompletionMessage(msg, codexSession.sessionId));
|
||||
const storedMcpCreateReply = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${mcpCreateReply.body.conversationId}.json`), 'utf8'));
|
||||
const storedMcpCreateSource = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexSession.sessionId}.json`), 'utf8'));
|
||||
assert(storedMcpCreateReply.messages.some((message) => message.crossConversation?.replyRequestId === mcpCreateReply.body.requestId), 'MCP create requestReply should persist waiting metadata on the new conversation');
|
||||
assert(storedMcpCreateSource.messages.some((message) => (
|
||||
message.crossConversation?.replyToRequestId === mcpCreateReply.body.requestId &&
|
||||
message.crossConversation?.processed === true &&
|
||||
message.crossConversation?.autoRun === false
|
||||
)), 'MCP create requestReply should send a processed display-only reply back to source');
|
||||
const storedMcpCreateReplyIndex = storedMcpCreateSource.messages.findIndex((message) => message.crossConversation?.replyToRequestId === mcpCreateReply.body.requestId);
|
||||
assert(storedMcpCreateReplyIndex >= 0, 'MCP create requestReply should send a processed display-only reply back to source');
|
||||
assert(storedMcpCreateSource.messages[storedMcpCreateReplyIndex].crossConversation?.processed === true, 'MCP create requestReply should mark the returned message processed');
|
||||
assert(storedMcpCreateSource.messages[storedMcpCreateReplyIndex].crossConversation?.autoRun === true, 'MCP create requestReply should mark the returned message as auto-run');
|
||||
assert(storedMcpCreateSource.messages.slice(storedMcpCreateReplyIndex + 1).some((message) => (
|
||||
message.role === 'assistant' &&
|
||||
/mcp create request reply/.test(String(message.content || '')) &&
|
||||
/子对话/.test(String(message.content || ''))
|
||||
)), 'MCP create requestReply should continue the source session after the child reply');
|
||||
|
||||
const crossTargetCwd = path.join(tempRoot, 'codex-mcp-cross-target');
|
||||
mkdirp(crossTargetCwd);
|
||||
@@ -934,7 +944,7 @@ async function main() {
|
||||
});
|
||||
assert(requestReply.status === 200 && requestReply.body?.ok, `MCP request reply should succeed: ${JSON.stringify(requestReply.body)}`);
|
||||
assert(requestReply.body.requestId && requestReply.body.status === 'waiting', 'MCP request reply should return a waiting request id');
|
||||
assert(requestReply.body.replyDelivery === 'display_only' && requestReply.body.sourceAutoRun === false, 'MCP request reply should declare display-only delivery without source auto-run');
|
||||
assert(requestReply.body.replyDelivery === 'auto_run' && requestReply.body.sourceAutoRun === true, 'MCP request reply should declare source auto-run delivery');
|
||||
const requestReplyTargetBubble = await nextMessage(messages, ws, (msg) => (
|
||||
msg.type === 'session_message' &&
|
||||
msg.sessionId === crossReplyTargetSession.sessionId &&
|
||||
@@ -951,6 +961,7 @@ async function main() {
|
||||
message.crossConversation?.processed === true
|
||||
))
|
||||
));
|
||||
await nextMessage(messages, ws, (msg) => isSessionCompletionMessage(msg, codexSession.sessionId));
|
||||
|
||||
const storedReplyTarget = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${crossReplyTargetSession.sessionId}.json`), 'utf8'));
|
||||
const storedReplySource = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${codexSession.sessionId}.json`), 'utf8'));
|
||||
@@ -963,15 +974,16 @@ async function main() {
|
||||
assert(storedReplyMessage.role === 'assistant', 'Returned cross message should be persisted as display-only assistant content');
|
||||
assert(storedReplyMessage.crossConversation.reply === true, 'Returned cross message should be marked as a reply');
|
||||
assert(storedReplyMessage.crossConversation.processed === true, 'Returned cross message should persist a processed marker');
|
||||
assert(storedReplyMessage.crossConversation.autoRun === false, 'Returned cross message should not auto-run the source session again');
|
||||
assert(storedReplyMessage.crossConversation.autoRun === true, 'Returned cross message should mark source auto-run');
|
||||
assert(storedReplyMessage.ccwebDisplayOnly === true, 'Returned cross message should be marked display-only');
|
||||
assert(/线程「/.test(storedReplyMessage.content || '') && /已返回消息/.test(storedReplyMessage.content || ''), 'Returned cross message should include reply heading');
|
||||
assert(/Codex mock handled/.test(storedReplyMessage.content || ''), 'Returned cross message should include target assistant output');
|
||||
assert(!storedReplySource.messages.slice(storedReplyMessageIndex + 1).some((message) => (
|
||||
assert(storedReplySource.messages.slice(storedReplyMessageIndex + 1).some((message) => (
|
||||
message.role === 'assistant' &&
|
||||
/Codex mock handled/.test(String(message.content || '')) &&
|
||||
/已返回消息/.test(String(message.content || ''))
|
||||
)), 'Returned cross message should not trigger the source session to run again');
|
||||
/cross reply requested/.test(String(message.content || '')) &&
|
||||
/子对话/.test(String(message.content || ''))
|
||||
)), 'Returned cross message should trigger the source session to run again');
|
||||
|
||||
const busySourceCwd = path.join(tempRoot, 'codex-mcp-busy-source');
|
||||
mkdirp(busySourceCwd);
|
||||
@@ -995,6 +1007,7 @@ async function main() {
|
||||
});
|
||||
assert(busyRequestReply.status === 200 && busyRequestReply.body?.ok, `MCP busy source request reply should succeed: ${JSON.stringify(busyRequestReply.body)}`);
|
||||
assert(busyRequestReply.body.requestId && busyRequestReply.body.status === 'waiting', 'Busy source request reply should return a waiting request id');
|
||||
assert(busyRequestReply.body.replyDelivery === 'auto_run' && busyRequestReply.body.sourceAutoRun === true, 'Busy source request reply should declare source auto-run delivery');
|
||||
await nextMessage(messages, ws, (msg) => msg.type === 'done' && msg.sessionId === busyReplyTargetSession.sessionId);
|
||||
await waitForJsonCondition(path.join(configDir, 'cross-conversation-replies.json'), (state) => (
|
||||
Array.isArray(state.replies) &&
|
||||
@@ -1038,7 +1051,7 @@ async function main() {
|
||||
assert(busySourceSummary?.status === 'running', 'MCP list should still mark the busy source as running before it completes');
|
||||
assert(busySourceSummary?.waitingOnChildren === true && busySourceSummary?.readyReplyCount === 1, 'MCP list should expose queued child replies on the source conversation');
|
||||
|
||||
await nextMessage(messages, ws, (msg) => msg.type === 'background_done' && msg.sessionId === busySourceSession.sessionId, 8000);
|
||||
await nextMessage(messages, ws, (msg) => isSessionCompletionMessage(msg, busySourceSession.sessionId), 8000);
|
||||
await waitForJsonCondition(path.join(sessionsDir, `${busySourceSession.sessionId}.json`), (session) => (
|
||||
Array.isArray(session.messages) &&
|
||||
session.messages.some((message) => (
|
||||
@@ -1051,6 +1064,14 @@ async function main() {
|
||||
const busyReplyIndex = storedBusySource.messages.findIndex((message) => message.crossConversation?.replyToRequestId === busyRequestReply.body.requestId);
|
||||
assert(busyReplyIndex > 0, 'Busy source should receive queued display-only reply after its run completes');
|
||||
assert(storedBusySource.messages[busyReplyIndex - 1]?.role === 'assistant' && /very slow cross-session prompt/.test(String(storedBusySource.messages[busyReplyIndex - 1].content || '')), 'Queued reply should be appended after the source run assistant message');
|
||||
assert(storedBusySource.messages[busyReplyIndex].crossConversation?.autoRun === true, 'Queued reply should mark source auto-run');
|
||||
await nextMessage(messages, ws, (msg) => isSessionCompletionMessage(msg, busySourceSession.sessionId), 8000);
|
||||
storedBusySource = JSON.parse(fs.readFileSync(path.join(sessionsDir, `${busySourceSession.sessionId}.json`), 'utf8'));
|
||||
assert(storedBusySource.messages.slice(busyReplyIndex + 1).some((message) => (
|
||||
message.role === 'assistant' &&
|
||||
/busy source reply requested/.test(String(message.content || '')) &&
|
||||
/子对话/.test(String(message.content || ''))
|
||||
)), 'Busy source should auto-run after the queued child reply is flushed');
|
||||
|
||||
const returnedPendingDetail = await callInternalMcp(port, internalMcpToken, {
|
||||
tool: 'ccweb_get_pending_reply',
|
||||
|
||||
Reference in New Issue
Block a user