修复多智能体模式下主智能体空闲时收不到子智能体输出推送的一系列 bug:
1. poll_multi_agent_notifications 死锁:原实现先等所有 running 实例
退出再 drain 消息池,导致 ask_master 在 await 期间永远等不到主对话
回答。改为池优先:pool 有消息立即派发,不管 running 状态。
2. _dispatch_multi_agent_idle_messages 缺 import:调用
inject_multi_agent_master_message 但文件顶部从未导入,NameError
被外层 except 吞掉,task 永远建不起来。
3. dispatch 内调试日志引用 rec 错位:dispatch_ma_idle_sender_user_message
被放到 create_chat_task 之前,触发 UnboundLocalError,task 同样建不起来。
4. session_data['auto_user_message_payload'] / preceding_user_notices
payload 漏写 auto_message_type:前端 isMultiAgentMessage() 只认
auto_message_type.startsWith('multi_agent_'),空字符串走 fallback
通知渲染。
5. dispatch 第①步重复持久化:对包含 last 的全部消息都调
inject_multi_agent_master_message 落盘,之后 task 又在 handle_task_with_sender
再 add_conversation,导致历史里出现两条相同 user 消息(前一条多智能体渲染,
后一条通知渲染)。前置 N-1 条只持久化一次,最后一条交给后续 task 自己持久化。
6. last 赋值时机错位:last_emit_payload 在 last=parsed_messages[-1] 之前引用,
UnboundLocalError 再次吃掉后续链路。
7. handle_task_with_sender 多智能体分支漏写 visibility='chat':
_user_message_ui_defaults('sub_agent') 默认 visibility='compact',
透传到落盘 metadata 后,前端从后端加载历史时走通知渲染分支。显式
user_message_metadata['visibility']='chat' 强制走多智能体专用渲染。
450 lines
16 KiB
TypeScript
450 lines
16 KiB
TypeScript
// @ts-nocheck
|
|
import { debugLog, goalModeDebugLog } from '../common';
|
|
import { useTaskStore } from '../../../stores/task';
|
|
import { getMessageVisibility, messageStartsWork } from '../../../utils/messageVisibility';
|
|
import {
|
|
debugNotifyLog,
|
|
keyNotifyLog,
|
|
jsonDebug,
|
|
userMDebug,
|
|
isRestoreDebugEnabled,
|
|
restoreDebugLog,
|
|
isSystemAutoUserMessagePayload,
|
|
isRuntimeModeNoticePayload,
|
|
resolveUserMessageSource,
|
|
resolveUserMessageMetadata,
|
|
isEmptyAssistantPlaceholderMessage,
|
|
getOptimisticUserEchoTarget,
|
|
findRecentMatchingUserMessage,
|
|
} from './shared';
|
|
|
|
export const probeMethods = {
|
|
scheduleTodoListRefresh(delayMs = 120) {
|
|
const delay = Number.isFinite(delayMs) ? Math.max(0, Number(delayMs)) : 120;
|
|
if (this._todoRefreshTimer) {
|
|
clearTimeout(this._todoRefreshTimer);
|
|
this._todoRefreshTimer = null;
|
|
}
|
|
this._todoRefreshTimer = setTimeout(() => {
|
|
this._todoRefreshTimer = null;
|
|
if (typeof this.fetchTodoList === 'function') {
|
|
Promise.resolve(this.fetchTodoList()).catch(() => {});
|
|
}
|
|
}, delay);
|
|
},
|
|
stopWaitingTaskProbe() {
|
|
if (this.waitingTaskProbeTimer) {
|
|
debugNotifyLog('[DEBUG_NOTIFY][ui] stopWaitingTaskProbe');
|
|
clearInterval(this.waitingTaskProbeTimer);
|
|
this.waitingTaskProbeTimer = null;
|
|
}
|
|
this._waitingProbeStableEmptyCount = 0;
|
|
},
|
|
startWaitingTaskProbe() {
|
|
if (this.waitingTaskProbeTimer) {
|
|
debugNotifyLog('[DEBUG_NOTIFY][ui] startWaitingTaskProbe:already-started');
|
|
return;
|
|
}
|
|
debugNotifyLog('[DEBUG_NOTIFY][ui] startWaitingTaskProbe');
|
|
keyNotifyLog('[DEBUG_NOTIFY_KEY][ui] startWaitingTaskProbe', {
|
|
conversationId: this.currentConversationId
|
|
});
|
|
this.waitingTaskProbeTimer = setInterval(async () => {
|
|
if (!this.waitingForSubAgent) {
|
|
this.stopWaitingTaskProbe();
|
|
return;
|
|
}
|
|
const resumed = await this.tryResumeWaitingNoticeTask();
|
|
if (resumed) {
|
|
debugNotifyLog('[DEBUG_NOTIFY][ui] waitingTaskProbe:resumed-and-stop');
|
|
keyNotifyLog('[DEBUG_NOTIFY_KEY][ui] waitingTaskProbe:resumed-and-stop');
|
|
this.stopWaitingTaskProbe();
|
|
return;
|
|
}
|
|
|
|
const state = await this.inspectWaitingCompletionState();
|
|
if (state.hasPending) {
|
|
this._waitingProbeStableEmptyCount = 0;
|
|
this.waitingForBackgroundCommand = !!state.hasBackgroundPending;
|
|
return;
|
|
}
|
|
|
|
this._waitingProbeStableEmptyCount = (this._waitingProbeStableEmptyCount || 0) + 1;
|
|
if (this._waitingProbeStableEmptyCount >= 2) {
|
|
debugLog('[TaskPolling] 等待态兜底清理:未发现运行中的后台子智能体/后台指令');
|
|
this.streamingMessage = false;
|
|
this.taskInProgress = false;
|
|
this.stopRequested = false;
|
|
this.waitingForSubAgent = false;
|
|
this.waitingForBackgroundCommand = false;
|
|
if (typeof this.clearPendingTools === 'function') {
|
|
this.clearPendingTools('waiting_probe_auto_clear');
|
|
}
|
|
this.clearTaskState();
|
|
this.stopWaitingTaskProbe();
|
|
this.$forceUpdate();
|
|
}
|
|
}, 1000);
|
|
},
|
|
|
|
// ---------- 多智能体任务探测 ----------
|
|
stopMultiAgentTaskProbe() {
|
|
if (this.multiAgentTaskProbeTimer) {
|
|
debugNotifyLog('[DEBUG_NOTIFY][ui] stopMultiAgentTaskProbe');
|
|
goalModeDebugLog('probe.stopMultiAgentTaskProbe', {
|
|
conversationId: this.currentConversationId,
|
|
taskInProgress: this.taskInProgress
|
|
});
|
|
clearInterval(this.multiAgentTaskProbeTimer);
|
|
this.multiAgentTaskProbeTimer = null;
|
|
}
|
|
},
|
|
startMultiAgentTaskProbe() {
|
|
if (this.multiAgentTaskProbeTimer) {
|
|
debugNotifyLog('[DEBUG_NOTIFY][ui] startMultiAgentTaskProbe:already-started');
|
|
return;
|
|
}
|
|
debugNotifyLog('[DEBUG_NOTIFY][ui] startMultiAgentTaskProbe');
|
|
keyNotifyLog('[DEBUG_NOTIFY_KEY][ui] startMultiAgentTaskProbe', {
|
|
conversationId: this.currentConversationId
|
|
});
|
|
goalModeDebugLog('probe.startMultiAgentTaskProbe', {
|
|
conversationId: this.currentConversationId,
|
|
taskInProgress: this.taskInProgress
|
|
});
|
|
this.multiAgentTaskProbeTimer = setInterval(async () => {
|
|
if (!this.taskInProgress || !this.currentConversationId) {
|
|
debugNotifyLog('[DEBUG_NOTIFY][ui] multiAgentTaskProbe:stop-no-task-in-progress');
|
|
goalModeDebugLog('probe.multiAgentTaskProbe_stop', {
|
|
reason: 'no_task_in_progress',
|
|
taskInProgress: this.taskInProgress,
|
|
currentConversationId: this.currentConversationId
|
|
});
|
|
this.stopMultiAgentTaskProbe();
|
|
return;
|
|
}
|
|
goalModeDebugLog('probe.multiAgentTaskProbe_tick', {
|
|
currentConversationId: this.currentConversationId
|
|
});
|
|
const resumed = await this.tryResumeRunningTask();
|
|
if (resumed) {
|
|
debugNotifyLog('[DEBUG_NOTIFY][ui] multiAgentTaskProbe:resumed-and-stop');
|
|
keyNotifyLog('[DEBUG_NOTIFY_KEY][ui] multiAgentTaskProbe:resumed-and-stop');
|
|
this.stopMultiAgentTaskProbe();
|
|
}
|
|
}, 1000);
|
|
},
|
|
async tryResumeRunningTask() {
|
|
if (!this.currentConversationId) {
|
|
return false;
|
|
}
|
|
try {
|
|
const { useTaskStore } = await import('../../../stores/task');
|
|
const taskStore = useTaskStore();
|
|
if (taskStore.hasActiveTask) {
|
|
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeRunningTask:already-active', {
|
|
taskId: taskStore.currentTaskId,
|
|
status: taskStore.taskStatus
|
|
});
|
|
return true;
|
|
}
|
|
|
|
const resp = await fetch('/api/tasks');
|
|
if (!resp.ok) {
|
|
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeRunningTask:tasks-api-not-ok', {
|
|
status: resp.status
|
|
});
|
|
return false;
|
|
}
|
|
const result = await resp.json().catch(() => ({}));
|
|
const tasks = Array.isArray(result?.data) ? result.data : [];
|
|
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeRunningTask:tasks', {
|
|
currentConversationId: this.currentConversationId,
|
|
count: tasks.length,
|
|
running: tasks
|
|
.filter((t: any) => t?.status === 'running')
|
|
.map((t: any) => ({
|
|
task_id: t?.task_id,
|
|
conversation_id: t?.conversation_id,
|
|
status: t?.status
|
|
}))
|
|
});
|
|
const runningTask = tasks.find(
|
|
(task: any) =>
|
|
(task?.status === 'running' || task?.status === 'pending') &&
|
|
task?.conversation_id === this.currentConversationId &&
|
|
(!(this.versioningHostMode || this.dockerProjectMode) ||
|
|
!this.currentHostWorkspaceId ||
|
|
task?.workspace_id === this.currentHostWorkspaceId)
|
|
);
|
|
if (!runningTask?.task_id) {
|
|
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeRunningTask:no-matched-task');
|
|
goalModeDebugLog('probe.tryResumeRunningTask_no_match', {
|
|
currentConversationId: this.currentConversationId,
|
|
tasksCount: tasks.length
|
|
});
|
|
return false;
|
|
}
|
|
|
|
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeRunningTask:resume', {
|
|
task_id: runningTask.task_id
|
|
});
|
|
goalModeDebugLog('probe.tryResumeRunningTask_resume', {
|
|
taskId: runningTask.task_id,
|
|
conversationId: this.currentConversationId
|
|
});
|
|
keyNotifyLog('[DEBUG_NOTIFY_KEY][ui] tryResumeRunningTask:resume', {
|
|
task_id: runningTask.task_id,
|
|
conversationId: this.currentConversationId
|
|
});
|
|
if (typeof this.clearProcessedEvents === 'function') {
|
|
this.clearProcessedEvents();
|
|
}
|
|
taskStore.resumeTask(runningTask.task_id, {
|
|
status: 'running',
|
|
resetOffset: true,
|
|
eventHandler: (event: any) => this.handleTaskEvent(event)
|
|
});
|
|
return true;
|
|
} catch (error) {
|
|
console.warn('[TaskPolling] 接管运行中任务失败:', error);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
async inspectWaitingCompletionState() {
|
|
const terminalStatuses = new Set(['completed', 'failed', 'timeout', 'terminated', 'cancelled']);
|
|
let hasSubAgentPending = false;
|
|
let hasBackgroundPending = false;
|
|
let resolved = true;
|
|
|
|
try {
|
|
const [subResp, bgResp] = await Promise.all([
|
|
fetch('/api/sub_agents'),
|
|
fetch('/api/background_commands?limit=200')
|
|
]);
|
|
|
|
if (subResp.ok) {
|
|
const subResult = await subResp.json().catch(() => ({}));
|
|
const tasks = Array.isArray(subResult?.data) ? subResult.data : [];
|
|
const relevantTasks = tasks.filter((task: any) => {
|
|
if (!task) return false;
|
|
if (task.conversation_id && task.conversation_id !== this.currentConversationId)
|
|
return false;
|
|
return true;
|
|
});
|
|
hasSubAgentPending = relevantTasks.some((task: any) => {
|
|
if (!task?.run_in_background) return false;
|
|
const status = String(task?.status || '').toLowerCase();
|
|
return !terminalStatuses.has(status) || !!task?.notice_pending;
|
|
});
|
|
} else {
|
|
resolved = false;
|
|
}
|
|
|
|
if (bgResp.ok) {
|
|
const bgResult = await bgResp.json().catch(() => ({}));
|
|
const commands = Array.isArray(bgResult?.data) ? bgResult.data : [];
|
|
hasBackgroundPending = commands.some((command: any) => {
|
|
if (!command) return false;
|
|
const status = String(command?.status || '').toLowerCase();
|
|
return !terminalStatuses.has(status) || !!command?.notice_pending;
|
|
});
|
|
} else {
|
|
resolved = false;
|
|
}
|
|
} catch (error) {
|
|
debugNotifyLog('[DEBUG_NOTIFY][ui] inspectWaitingCompletionState:error', {
|
|
error: error?.message || String(error)
|
|
});
|
|
return {
|
|
resolved: false,
|
|
hasPending: true,
|
|
hasBackgroundPending: this.waitingForBackgroundCommand
|
|
};
|
|
}
|
|
|
|
return {
|
|
resolved,
|
|
hasPending: !resolved || hasSubAgentPending || hasBackgroundPending,
|
|
hasBackgroundPending
|
|
};
|
|
},
|
|
async tryResumeWaitingNoticeTask() {
|
|
if (!this.waitingForSubAgent || !this.currentConversationId) {
|
|
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeWaitingNoticeTask:skip', {
|
|
waitingForSubAgent: this.waitingForSubAgent,
|
|
currentConversationId: this.currentConversationId
|
|
});
|
|
return false;
|
|
}
|
|
try {
|
|
const { useTaskStore } = await import('../../../stores/task');
|
|
const taskStore = useTaskStore();
|
|
if (taskStore.hasActiveTask) {
|
|
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeWaitingNoticeTask:already-active', {
|
|
taskId: taskStore.currentTaskId,
|
|
status: taskStore.taskStatus
|
|
});
|
|
return true;
|
|
}
|
|
|
|
const resp = await fetch('/api/tasks');
|
|
if (!resp.ok) {
|
|
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeWaitingNoticeTask:tasks-api-not-ok', {
|
|
status: resp.status
|
|
});
|
|
return false;
|
|
}
|
|
const result = await resp.json().catch(() => ({}));
|
|
const tasks = Array.isArray(result?.data) ? result.data : [];
|
|
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeWaitingNoticeTask:tasks', {
|
|
currentConversationId: this.currentConversationId,
|
|
count: tasks.length,
|
|
running: tasks
|
|
.filter((t: any) => t?.status === 'running')
|
|
.map((t: any) => ({
|
|
task_id: t?.task_id,
|
|
conversation_id: t?.conversation_id,
|
|
status: t?.status
|
|
}))
|
|
});
|
|
const runningTask = tasks.find(
|
|
(task: any) =>
|
|
task?.status === 'running' &&
|
|
task?.conversation_id === this.currentConversationId &&
|
|
(!(this.versioningHostMode || this.dockerProjectMode) ||
|
|
!this.currentHostWorkspaceId ||
|
|
task?.workspace_id === this.currentHostWorkspaceId)
|
|
);
|
|
if (!runningTask?.task_id) {
|
|
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeWaitingNoticeTask:no-matched-task');
|
|
return false;
|
|
}
|
|
|
|
debugNotifyLog('[DEBUG_NOTIFY][ui] tryResumeWaitingNoticeTask:resume', {
|
|
task_id: runningTask.task_id
|
|
});
|
|
keyNotifyLog('[DEBUG_NOTIFY_KEY][ui] tryResumeWaitingNoticeTask:resume', {
|
|
task_id: runningTask.task_id,
|
|
conversationId: this.currentConversationId
|
|
});
|
|
// 关键修复:切到新的通知任务前,清空旧任务事件去重索引。
|
|
// 否则新任务 event idx 与旧任务重叠时会被误判为“重复事件”而丢失 user_message。
|
|
if (typeof this.clearProcessedEvents === 'function') {
|
|
this.clearProcessedEvents();
|
|
}
|
|
taskStore.resumeTask(runningTask.task_id, {
|
|
status: 'running',
|
|
resetOffset: true,
|
|
eventHandler: (event: any) => this.handleTaskEvent(event)
|
|
});
|
|
return true;
|
|
} catch (error) {
|
|
console.warn('[TaskPolling] 接管等待任务失败:', error);
|
|
return false;
|
|
}
|
|
},
|
|
async restoreSubAgentWaitingState(retry = 0) {
|
|
try {
|
|
if (!this.currentConversationId) {
|
|
if (retry < 5) {
|
|
setTimeout(() => {
|
|
this.restoreSubAgentWaitingState(retry + 1);
|
|
}, 300);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const response = await fetch('/api/sub_agents');
|
|
if (!response.ok) {
|
|
debugLog('[TaskPolling] 获取子智能体状态失败');
|
|
return;
|
|
}
|
|
const result = await response.json();
|
|
if (!result.success) {
|
|
debugLog('[TaskPolling] 子智能体状态响应无效');
|
|
return;
|
|
}
|
|
|
|
const tasks = Array.isArray(result.data) ? result.data : [];
|
|
debugLog('[TaskPolling] 子智能体状态响应', {
|
|
total: tasks.length,
|
|
currentConversationId: this.currentConversationId,
|
|
tasks: tasks.map((task: any) => ({
|
|
task_id: task?.task_id,
|
|
status: task?.status,
|
|
run_in_background: task?.run_in_background,
|
|
notice_pending: task?.notice_pending,
|
|
conversation_id: task?.conversation_id
|
|
}))
|
|
});
|
|
const terminalStatuses = new Set(['completed', 'failed', 'timeout', 'terminated']);
|
|
const relevant = tasks.filter((task: any) => {
|
|
if (task && task.conversation_id && task.conversation_id !== this.currentConversationId) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
const running = relevant.filter(
|
|
(task: any) => task?.run_in_background && !terminalStatuses.has(task?.status)
|
|
);
|
|
const pendingNotice = relevant.filter((task: any) => task?.notice_pending);
|
|
let backgroundPending = false;
|
|
try {
|
|
const bgResp = await fetch('/api/background_commands?limit=200');
|
|
if (bgResp.ok) {
|
|
const bgResult = await bgResp.json().catch(() => ({}));
|
|
const bgCommands = Array.isArray(bgResult?.data) ? bgResult.data : [];
|
|
backgroundPending = bgCommands.some((command: any) => {
|
|
if (!command) return false;
|
|
const status = String(command?.status || '').toLowerCase();
|
|
return !terminalStatuses.has(status) || !!command?.notice_pending;
|
|
});
|
|
}
|
|
} catch (bgErr) {
|
|
console.warn('[TaskPolling] 获取后台指令状态失败:', bgErr);
|
|
}
|
|
|
|
if (running.length || pendingNotice.length || backgroundPending) {
|
|
debugLog('[TaskPolling] 恢复子智能体等待状态', {
|
|
running: running.length,
|
|
pendingNotice: pendingNotice.length,
|
|
backgroundPending,
|
|
runningTasks: running.map((task: any) => ({
|
|
task_id: task?.task_id,
|
|
status: task?.status
|
|
})),
|
|
pendingTasks: pendingNotice.map((task: any) => ({
|
|
task_id: task?.task_id,
|
|
status: task?.status
|
|
}))
|
|
});
|
|
this.waitingForSubAgent = true;
|
|
this.waitingForBackgroundCommand = backgroundPending;
|
|
this.taskInProgress = true;
|
|
this.streamingMessage = false;
|
|
this.stopRequested = false;
|
|
this.startWaitingTaskProbe();
|
|
this.$forceUpdate();
|
|
} else {
|
|
this.waitingForSubAgent = false;
|
|
this.waitingForBackgroundCommand = false;
|
|
this.stopWaitingTaskProbe();
|
|
if (!this.streamingMessage) {
|
|
this.taskInProgress = false;
|
|
}
|
|
this.$forceUpdate();
|
|
}
|
|
} catch (error) {
|
|
console.error('[TaskPolling] 恢复子智能体等待状态失败:', error);
|
|
}
|
|
},
|
|
clearProcessedEvents() {
|
|
if (this._processedEventIndices) {
|
|
this._processedEventIndices.clear();
|
|
}
|
|
}
|
|
};
|