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