diff --git a/src/command/handlers/list_todos.rs b/src/command/handlers/list_todos.rs index 98f429f..c297b6d 100644 --- a/src/command/handlers/list_todos.rs +++ b/src/command/handlers/list_todos.rs @@ -41,10 +41,10 @@ impl CommandHandler for ListTodosCommandHandler { _ => None, }; - // 子代理:scope_key = sub:{parent_session_id}:{task_id} + // 子代理:scope_key = task_id(全局唯一,与 todo_write 保持一致) // 主代理:scope_key = topic_id.unwrap_or(session_id) - let scope_key = if let (Some(tid), Some(parent_sid)) = (task_id.as_deref(), ctx.session_id.as_deref()) { - format!("sub:{}:{}", parent_sid, tid) + let scope_key = if let Some(tid) = task_id.as_deref() { + tid.to_string() } else { ctx.topic_id .as_deref() diff --git a/src/tools/todo_write.rs b/src/tools/todo_write.rs index af04bdb..8e0608c 100644 --- a/src/tools/todo_write.rs +++ b/src/tools/todo_write.rs @@ -330,10 +330,14 @@ impl Tool for TodoWriteTool { /// 计算 scope_key: /// - 主代理 (nesting_depth == 0):优先 topic_id,否则 session_id -/// - 子/孙代理 (nesting_depth > 0):使用 session_id 隔离,避免污染父代理 todo +/// - 子/孙代理 (nesting_depth > 0):使用 task_id 隔离(全局唯一,与 list_todos 保持一致) pub(crate) fn scope_key_from_context(context: &ToolContext) -> Option { if context.nesting_depth > 0 { - context.session_id.clone().filter(|s| !s.is_empty()) + // 使用 task_id 而不是 session_id 作为 scope_key。 + // session_id 对于孙智能体包含父链(如 sub:sub:root:parent:task), + // 而 list_todos handler 用根 session + task_id 拼接,两者不匹配。 + // task_id 是全局唯一的 UUID(task:xxx),直接使用可避免层级不一致。 + context.task_id.clone().filter(|s| !s.is_empty()) } else { let tid = context.topic_id.as_deref().filter(|t| !t.is_empty()); let sid = context.session_id.as_deref().filter(|s| !s.is_empty()); diff --git a/web/src/components/Chat/MessageBubble.tsx b/web/src/components/Chat/MessageBubble.tsx index fc1de64..9fe6555 100644 --- a/web/src/components/Chat/MessageBubble.tsx +++ b/web/src/components/Chat/MessageBubble.tsx @@ -364,7 +364,6 @@ export function MessageBubble({ message, onNavigateToSubAgent, showThinking = tr const isTaskTool = message.toolName === 'task' const taskResult = isTaskTool && hasResult ? parseTaskResult(displayContent) : null - const isSubAgent = !!message.subagentTaskId const subagentType = (message.arguments as Record | null)?.subagent_type as string || 'general' const taskDescription = (message.arguments as Record | null)?.description as string || '' const taskPrompt = (message.arguments as Record | null)?.prompt as string || '' diff --git a/web/src/hooks/useChat.ts b/web/src/hooks/useChat.ts index 0aeb2dc..17e94e7 100644 --- a/web/src/hooks/useChat.ts +++ b/web/src/hooks/useChat.ts @@ -395,6 +395,34 @@ export function useChat(): UseChatReturn { return } + // Backfill grandchild task_id on task tool_call in sub-agent view. + // When the sub-agent spawns a grandchild via the task tool, the + // protocol's subagent_task_id field carries the parent task_id for + // routing, not the new child task_id. We must update it to the + // child's task_id from the task_started event so that "查看实时进度" + // navigates to the correct (grandchild) session. + if (message.type === 'task_started') { + const msg = message as TaskStarted + if (msg.parent_task_id === currentSubAgentView.taskId) { + setSubAgentStack((prev) => { + if (prev.length === 0) return prev + const top = prev[prev.length - 1] + const updatedMessages = [...top.messages] + for (let i = updatedMessages.length - 1; i >= 0; i--) { + const m = updatedMessages[i] + if (m.type === 'tool_call' && m.toolName === 'task' && m.subagentTaskId === currentSubAgentView.taskId) { + updatedMessages[i] = { ...m, subagentTaskId: msg.task_id } + break + } + } + const newStack = [...prev] + newStack[newStack.length - 1] = { ...top, messages: updatedMessages } + return newStack + }) + return + } + } + // Only accept messages explicitly tagged with matching subagent_task_id. // History messages are now tagged by the backend (send_task_messages), // and live sub-agent messages are tagged by SubAgentEmitter.