Skip to content

Commit ab4eff7

Browse files
committed
Merge branch 'fix/stop-second-call-race-condition' into 'master'
fix(feishu): /stop race condition — queue stuck & second stop fails See merge request ai_native/DeepVCode/DeepVcodeClient!501
2 parents 7a5596a + 1a66e85 commit ab4eff7

2 files changed

Lines changed: 453 additions & 1 deletion

File tree

packages/cli/src/ui/commands/feishuCommand.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,16 @@ const sideQuestionControllers = new Map<string, AbortController>();
796796
*/
797797
export const __testing_messageQueues = messageQueues;
798798

799+
/**
800+
* Export-only-for-tests: 把 isProcessingQueues / activeAbortControllers 暴露出来
801+
* 供 /stop 竞态 bug 的单测验证状态流转。生产代码不应使用这些出口。
802+
*/
803+
export const __testing_isProcessingQueues = isProcessingQueues;
804+
export const __testing_activeAbortControllers = activeAbortControllers;
805+
export const __testing_decrementProcessingCount = decrementProcessingCount;
806+
export const __testing_activeProcessingCount = { get: () => activeProcessingCount };
807+
export const __testing_processingChatIds = processingChatIds;
808+
799809
/**
800810
* Detect a `/btw` side-question command in raw Feishu message text.
801811
* Returns the trimmed question (without the `/btw` prefix) if matched,
@@ -2033,6 +2043,26 @@ async function handleFeishuCommand(
20332043
controller.abort();
20342044
if (chatId) {
20352045
activeAbortControllers.delete(chatId);
2046+
// FIX: /stop 必须同步递减处理计数,否则 UI 状态(如飞书卡片 footer)
2047+
// 仍显示「处理中」,且旧 finally 块的 decrementProcessingCount 会
2048+
// 误减新任务的计数
2049+
decrementProcessingCount(chatId);
2050+
// FIX: 清除该 chat 的队列处理状态,防止 /stop 后新消息被错误入队
2051+
// (/stop 只做了 abort + delete controller,遗漏了 isProcessingQueues 和
2052+
// messageQueues 的清理,导致竞态窗口期间新消息看到 isProcessing=true
2053+
// 被错误入队且无法自愈。详见 docs/bug-report-stop-queue-race-condition.md)
2054+
isProcessingQueues.set(chatId, false);
2055+
// FIX: 清空该 chat 的消息队列(拒绝任何排队中的消息)
2056+
// 注意:必须同时清空数组内容(splice(0)),否则旧 processMessageQueueForChat
2057+
// 的 while 循环仍持有数组引用,会继续处理已被 resolve(null) 的消息。
2058+
const pendingQueue = messageQueues.get(chatId);
2059+
if (pendingQueue) {
2060+
for (const item of pendingQueue) {
2061+
item.resolve(null);
2062+
}
2063+
pendingQueue.splice(0); // 清空数组内容,让旧 while 循环看到 length=0 后自然退出
2064+
messageQueues.delete(chatId);
2065+
}
20362066
} else {
20372067
activeAbortController = null;
20382068
}
@@ -3954,6 +3984,11 @@ async function handleStart(context?: CommandContext): Promise<string> {
39543984
return null;
39553985
} catch (err: any) {
39563986
if (err.name === 'AbortError' || err.message?.includes('aborted') || err.message?.includes('cancelled') || err.message?.includes('canceled')) {
3987+
// 注:不在此处清除 isProcessingQueues,因为 processMessageQueueForChat 的
3988+
// while 循环仍在运行。如果在 catch 中设置 isProcessingQueues=false,会破坏
3989+
// 防重入守卫,允许新消息并发进入同一 chat 的处理循环。
3990+
// isProcessingQueues 的清除统一由 processMessageQueueForChat 的 finally 负责,
3991+
// /stop 命令会提前设置 isProcessingQueues=false 来关闭竞态窗口。
39573992
if (activeCardId && streaming) {
39583993
const abortedFooterMetrics = await getFeishuStatusMetrics(config, geminiClient, lastRequestTokenUsage);
39593994
abortedFooterMetrics.status = '已中止';
@@ -3982,7 +4017,12 @@ async function handleStart(context?: CommandContext): Promise<string> {
39824017
emitFeishuMessageLog(msg.chatId, `❌ ${err.message}`, 'tool');
39834018
return errorReply;
39844019
} finally {
3985-
activeAbortControllers.delete(msg.chatId);
4020+
// FIX: 只删除属于本次任务调用的控制器,避免误删新任务的控制器
4021+
// (旧 finally 块异步执行时,新任务可能已注册了同 chatId 的新控制器)
4022+
const currentController = activeAbortControllers.get(msg.chatId);
4023+
if (currentController === abortController) {
4024+
activeAbortControllers.delete(msg.chatId);
4025+
}
39864026
decrementProcessingCount(msg.chatId);
39874027

39884028
// 💾 持久化本会话的 AI 客户端历史(对齐 CLI useSessionAutoSave)。飞书无 React
@@ -4332,6 +4372,13 @@ async function handleStart(context?: CommandContext): Promise<string> {
43324372
}
43334373
}
43344374
} finally {
4375+
// 注:isProcessingQueues 的清除在此处完成。/stop 命令会提前设置
4376+
// isProcessingQueues=false 来关闭竞态窗口,此处再次设置 false 是无害的
4377+
// (false → false)。理论上存在旧 finally 清除新处理标志的极窄竞态窗口
4378+
// (/stop 设 false → 新消息 B 设 true → 旧 finally 设 false),但实际概率
4379+
// 极低:旧 finally 在 AbortError 传播后几乎立即执行(微秒级),而新消息 B
4380+
// 需要经过飞书消息接收、入队、processMessageQueueForChat 入口检查等异步步骤
4381+
// 才能设置 true,时间差远大于旧 finally 的执行窗口。
43354382
isProcessingQueues.set(chatId, false);
43364383
}
43374384
}

0 commit comments

Comments
 (0)