fix: 同步前置 subAgentViewRef/subAgentStackRef,消除子智能体视图间歇性丢消息
根因: subAgentViewRef.current 在 setSubAgentStack updater 内部更新,updater 在 React render 阶段才执行,不是同步的。从 enterSubAgentView() 返回到 React render 之间存在竞态窗口,WebSocket 消息读到过时的 ref 被错误路由到主视图并丢弃。 修复: ref 赋值从 updater 内部移到 setSubAgentStack 之前同步执行。新增 subAgentStackRef 供 exit/navigate 在空依赖闭包中读取最新栈计算新栈顶。selectTopic/selectChannel/selectSession 清空栈时也同步设置 ref。updater 变纯函数,StrictMode 双调用无副作用。 Plan: fix-subagent-view-ref-race
This commit is contained in:
parent
6df87fe399
commit
b9c880d823
@ -181,6 +181,9 @@ export function useChat(): UseChatReturn {
|
||||
|
||||
// Ref to track subAgentView and schedulerView for use in callbacks
|
||||
const subAgentViewRef = useRef<SubAgentView | null>(null)
|
||||
// 同步追踪 subAgentStack,供 exit/navigate 在回调中读取最新栈计算新栈顶
|
||||
// ref 必须在事件处理器返回前同步更新,避免 WebSocket 消息路由竞态
|
||||
const subAgentStackRef = useRef<SubAgentView[]>([])
|
||||
const schedulerViewRef = useRef<SchedulerJobView | null>(null)
|
||||
const topicsRef = useRef<Topic[]>([])
|
||||
const selectedTopicRef = useRef<string | null>(null)
|
||||
@ -1007,6 +1010,8 @@ export function useChat(): UseChatReturn {
|
||||
const selectTopic = useCallback((topicId: string) => {
|
||||
setSelectedTopic(topicId)
|
||||
setMessages([])
|
||||
subAgentViewRef.current = null
|
||||
subAgentStackRef.current = []
|
||||
setSubAgentStack([])
|
||||
}, [])
|
||||
|
||||
@ -1053,6 +1058,8 @@ export function useChat(): UseChatReturn {
|
||||
setTopics([])
|
||||
setSelectedTopic(null)
|
||||
setMessages([])
|
||||
subAgentViewRef.current = null
|
||||
subAgentStackRef.current = []
|
||||
setSubAgentStack([])
|
||||
setIsLoading(true)
|
||||
}, [selectedChannel])
|
||||
@ -1063,6 +1070,8 @@ export function useChat(): UseChatReturn {
|
||||
setTopics([])
|
||||
setSelectedTopic(null)
|
||||
setMessages([])
|
||||
subAgentViewRef.current = null
|
||||
subAgentStackRef.current = []
|
||||
setSubAgentStack([])
|
||||
setIsLoading(true)
|
||||
}, [selectedSessionId])
|
||||
@ -1075,11 +1084,15 @@ export function useChat(): UseChatReturn {
|
||||
}
|
||||
}, [sessionId])
|
||||
|
||||
// Keep refs in sync with state
|
||||
// Keep refs in sync with state (兜底,确保 ref 与 state 最终一致)
|
||||
useEffect(() => {
|
||||
subAgentViewRef.current = subAgentView
|
||||
}, [subAgentView])
|
||||
|
||||
useEffect(() => {
|
||||
subAgentStackRef.current = subAgentStack
|
||||
}, [subAgentStack])
|
||||
|
||||
useEffect(() => {
|
||||
schedulerViewRef.current = schedulerView
|
||||
}, [schedulerView])
|
||||
@ -1100,12 +1113,12 @@ export function useChat(): UseChatReturn {
|
||||
status: 'loading',
|
||||
messages: [],
|
||||
}
|
||||
setSubAgentStack((prev) => {
|
||||
const newStack = [...prev, newView]
|
||||
// Sync ref immediately so WebSocket response routing works correctly
|
||||
subAgentViewRef.current = newView
|
||||
return newStack
|
||||
})
|
||||
// 同步设置 ref,消除竞态窗口:updater 在 React render 阶段才执行,
|
||||
// 期间 WebSocket 消息会读到过时的 ref。ref 必须在事件处理器返回前同步更新。
|
||||
subAgentViewRef.current = newView
|
||||
subAgentStackRef.current = [...subAgentStackRef.current, newView]
|
||||
// updater 变纯函数,不再有副作用
|
||||
setSubAgentStack((prev) => [...prev, newView])
|
||||
return {
|
||||
type: 'load_task_messages',
|
||||
task_id: taskId,
|
||||
@ -1113,42 +1126,45 @@ export function useChat(): UseChatReturn {
|
||||
}, [])
|
||||
|
||||
const exitSubAgentView = useCallback((): Command | null => {
|
||||
let command: Command | null = null
|
||||
setSubAgentStack((prev) => {
|
||||
if (prev.length <= 1) {
|
||||
subAgentViewRef.current = null
|
||||
return []
|
||||
}
|
||||
const newStack = prev.slice(0, -1)
|
||||
const newTop = newStack[newStack.length - 1]
|
||||
subAgentViewRef.current = newTop
|
||||
// 清空目标层 messages + status 置 loading,并准备重新拉取命令
|
||||
const clearedStack = [...newStack]
|
||||
clearedStack[clearedStack.length - 1] = { ...newTop, messages: [], status: 'loading' }
|
||||
command = { type: 'load_task_messages', task_id: newTop.taskId }
|
||||
return clearedStack
|
||||
})
|
||||
return command
|
||||
// 从 ref 读取最新栈(useCallback 依赖为空,闭包中的 subAgentStack 是旧值)
|
||||
const current = subAgentStackRef.current
|
||||
if (current.length <= 1) {
|
||||
subAgentViewRef.current = null
|
||||
subAgentStackRef.current = []
|
||||
setSubAgentStack([])
|
||||
return null
|
||||
}
|
||||
const newStack = current.slice(0, -1)
|
||||
const newTop = newStack[newStack.length - 1]
|
||||
// 同步设置 ref,消除竞态窗口
|
||||
subAgentViewRef.current = newTop
|
||||
// 清空目标层 messages + status 置 loading,准备重新拉取
|
||||
const clearedStack = [...newStack]
|
||||
clearedStack[clearedStack.length - 1] = { ...newTop, messages: [], status: 'loading' }
|
||||
subAgentStackRef.current = clearedStack
|
||||
setSubAgentStack(clearedStack)
|
||||
return { type: 'load_task_messages', task_id: newTop.taskId }
|
||||
}, [])
|
||||
|
||||
const navigateToSubAgentLevel = useCallback((index: number): Command | null => {
|
||||
let command: Command | null = null
|
||||
setSubAgentStack((prev) => {
|
||||
if (index < 0) {
|
||||
// -1 means go back to main session (clear all)
|
||||
subAgentViewRef.current = null
|
||||
return []
|
||||
}
|
||||
if (index >= prev.length) return prev
|
||||
const newStack = prev.slice(0, index + 1)
|
||||
const newTop = newStack[newStack.length - 1]
|
||||
subAgentViewRef.current = newTop
|
||||
const clearedStack = [...newStack]
|
||||
clearedStack[clearedStack.length - 1] = { ...newTop, messages: [], status: 'loading' }
|
||||
command = { type: 'load_task_messages', task_id: newTop.taskId }
|
||||
return clearedStack
|
||||
})
|
||||
return command
|
||||
const current = subAgentStackRef.current
|
||||
if (index < 0) {
|
||||
// -1 means go back to main session (clear all)
|
||||
subAgentViewRef.current = null
|
||||
subAgentStackRef.current = []
|
||||
setSubAgentStack([])
|
||||
return null
|
||||
}
|
||||
if (index >= current.length) return null
|
||||
const newStack = current.slice(0, index + 1)
|
||||
const newTop = newStack[newStack.length - 1]
|
||||
// 同步设置 ref,消除竞态窗口
|
||||
subAgentViewRef.current = newTop
|
||||
const clearedStack = [...newStack]
|
||||
clearedStack[clearedStack.length - 1] = { ...newTop, messages: [], status: 'loading' }
|
||||
subAgentStackRef.current = clearedStack
|
||||
setSubAgentStack(clearedStack)
|
||||
return { type: 'load_task_messages', task_id: newTop.taskId }
|
||||
}, [])
|
||||
|
||||
// 记忆方法
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user