修复新会话可能泄漏问题
This commit is contained in:
@@ -964,7 +964,33 @@ export function ConversationDetailPanel() {
|
|||||||
})
|
})
|
||||||
}, [onPreviewTabReplaced, disconnectByKey])
|
}, [onPreviewTabReplaced, disconnectByKey])
|
||||||
|
|
||||||
// Background turn_complete handler: for conversations not open in tabs
|
// Refs for background turn_complete handler so the listener
|
||||||
|
// can be registered once and always read the latest values.
|
||||||
|
const getConversationIdByExternalIdRef = useRef(getConversationIdByExternalId)
|
||||||
|
const getSessionRef = useRef(getSession)
|
||||||
|
const runtimeCompleteTurnRef = useRef(runtimeCompleteTurn)
|
||||||
|
const runtimeRemoveConversationRef = useRef(runtimeRemoveConversation)
|
||||||
|
const refreshConversationsRef = useRef(refreshConversations)
|
||||||
|
useEffect(() => {
|
||||||
|
getConversationIdByExternalIdRef.current = getConversationIdByExternalId
|
||||||
|
}, [getConversationIdByExternalId])
|
||||||
|
useEffect(() => {
|
||||||
|
getSessionRef.current = getSession
|
||||||
|
}, [getSession])
|
||||||
|
useEffect(() => {
|
||||||
|
runtimeCompleteTurnRef.current = runtimeCompleteTurn
|
||||||
|
}, [runtimeCompleteTurn])
|
||||||
|
useEffect(() => {
|
||||||
|
runtimeRemoveConversationRef.current = runtimeRemoveConversation
|
||||||
|
}, [runtimeRemoveConversation])
|
||||||
|
useEffect(() => {
|
||||||
|
refreshConversationsRef.current = refreshConversations
|
||||||
|
}, [refreshConversations])
|
||||||
|
|
||||||
|
// Background turn_complete handler: for conversations not open in tabs.
|
||||||
|
// Registered once — uses refs to avoid re-creating the listener on every
|
||||||
|
// state change, which would cause "Couldn't find callback id" warnings
|
||||||
|
// due to the async gap between unlisten and the new listen().
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
let unlisten: (() => void | Promise<void>) | null = null
|
let unlisten: (() => void | Promise<void>) | null = null
|
||||||
@@ -975,9 +1001,8 @@ export function ConversationDetailPanel() {
|
|||||||
const payload = event.payload
|
const payload = event.payload
|
||||||
if (payload.type !== "turn_complete") return
|
if (payload.type !== "turn_complete") return
|
||||||
|
|
||||||
const runtimeConversationId = getConversationIdByExternalId(
|
const runtimeConversationId =
|
||||||
payload.session_id
|
getConversationIdByExternalIdRef.current(payload.session_id)
|
||||||
)
|
|
||||||
const summary = conversationsRef.current.find(
|
const summary = conversationsRef.current.find(
|
||||||
(item) => item.external_id === payload.session_id
|
(item) => item.external_id === payload.session_id
|
||||||
)
|
)
|
||||||
@@ -997,12 +1022,12 @@ export function ConversationDetailPanel() {
|
|||||||
if (isOpenInTabs) return
|
if (isOpenInTabs) return
|
||||||
|
|
||||||
// Promote liveMessage + optimisticTurns to localTurns immediately
|
// Promote liveMessage + optimisticTurns to localTurns immediately
|
||||||
runtimeCompleteTurn(matchedConversationId)
|
runtimeCompleteTurnRef.current(matchedConversationId)
|
||||||
|
|
||||||
// If tab was closed while agent was responding, clean up now
|
// If tab was closed while agent was responding, clean up now
|
||||||
const session = getSession(matchedConversationId)
|
const session = getSessionRef.current(matchedConversationId)
|
||||||
if (session?.pendingCleanup) {
|
if (session?.pendingCleanup) {
|
||||||
runtimeRemoveConversation(matchedConversationId)
|
runtimeRemoveConversationRef.current(matchedConversationId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update conversation status — use the DB summary (found by
|
// Update conversation status — use the DB summary (found by
|
||||||
@@ -1013,7 +1038,7 @@ export function ConversationDetailPanel() {
|
|||||||
(matchedConversationId > 0 ? matchedConversationId : null)
|
(matchedConversationId > 0 ? matchedConversationId : null)
|
||||||
if (dbId && (!summary || summary.status === "in_progress")) {
|
if (dbId && (!summary || summary.status === "in_progress")) {
|
||||||
updateConversationStatus(dbId, "pending_review")
|
updateConversationStatus(dbId, "pending_review")
|
||||||
.then(() => refreshConversations())
|
.then(() => refreshConversationsRef.current())
|
||||||
.catch((error: unknown) =>
|
.catch((error: unknown) =>
|
||||||
console.error(
|
console.error(
|
||||||
"[ConversationDetailPanel] background update status:",
|
"[ConversationDetailPanel] background update status:",
|
||||||
@@ -1044,13 +1069,7 @@ export function ConversationDetailPanel() {
|
|||||||
"ConversationDetailPanel.backgroundRefresh"
|
"ConversationDetailPanel.backgroundRefresh"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [
|
}, [])
|
||||||
getConversationIdByExternalId,
|
|
||||||
getSession,
|
|
||||||
runtimeCompleteTurn,
|
|
||||||
runtimeRemoveConversation,
|
|
||||||
refreshConversations,
|
|
||||||
])
|
|
||||||
|
|
||||||
const hasNoTabs = tabs.length === 0 && !activeTabId
|
const hasNoTabs = tabs.length === 0 && !activeTabId
|
||||||
const activeConversationTab = useMemo(
|
const activeConversationTab = useMemo(
|
||||||
|
|||||||
@@ -1149,6 +1149,8 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
// Guard against concurrent connect() calls
|
// Guard against concurrent connect() calls
|
||||||
const connectingKeysRef = useRef(new Set<string>())
|
const connectingKeysRef = useRef(new Set<string>())
|
||||||
|
// Keys whose disconnect was requested while connect was still in flight
|
||||||
|
const abandonedKeysRef = useRef(new Set<string>())
|
||||||
|
|
||||||
type AutoLinkBlockState =
|
type AutoLinkBlockState =
|
||||||
| { kind: "none"; reason: "" }
|
| { kind: "none"; reason: "" }
|
||||||
@@ -1772,6 +1774,14 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
await waitForListenerReady()
|
await waitForListenerReady()
|
||||||
const connectionId = await acpConnect(agentType, workingDir, sessionId)
|
const connectionId = await acpConnect(agentType, workingDir, sessionId)
|
||||||
|
|
||||||
|
// If disconnect was requested while connect was in flight,
|
||||||
|
// tear down immediately instead of registering the connection.
|
||||||
|
if (abandonedKeysRef.current.delete(contextKey)) {
|
||||||
|
acpDisconnect(connectionId).catch(() => {})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
reverseMapRef.current.set(connectionId, contextKey)
|
reverseMapRef.current.set(connectionId, contextKey)
|
||||||
lastActivityRef.current.set(contextKey, Date.now())
|
lastActivityRef.current.set(contextKey, Date.now())
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -1800,6 +1810,7 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
|||||||
throw err
|
throw err
|
||||||
} finally {
|
} finally {
|
||||||
connectingKeysRef.current.delete(contextKey)
|
connectingKeysRef.current.delete(contextKey)
|
||||||
|
abandonedKeysRef.current.delete(contextKey)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@@ -1816,7 +1827,14 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
|||||||
const disconnect = useCallback(
|
const disconnect = useCallback(
|
||||||
async (contextKey: string) => {
|
async (contextKey: string) => {
|
||||||
const conn = storeRef.current.connections.get(contextKey)
|
const conn = storeRef.current.connections.get(contextKey)
|
||||||
if (!conn) return
|
if (!conn) {
|
||||||
|
// connect() is still in flight — mark as abandoned so it
|
||||||
|
// tears down immediately when acpConnect returns.
|
||||||
|
if (connectingKeysRef.current.has(contextKey)) {
|
||||||
|
abandonedKeysRef.current.add(contextKey)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
await acpDisconnect(conn.connectionId)
|
await acpDisconnect(conn.connectionId)
|
||||||
reverseMapRef.current.delete(conn.connectionId)
|
reverseMapRef.current.delete(conn.connectionId)
|
||||||
lastActivityRef.current.delete(contextKey)
|
lastActivityRef.current.delete(contextKey)
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export function useConnectionLifecycle({
|
|||||||
status,
|
status,
|
||||||
selectorsReady,
|
selectorsReady,
|
||||||
connect: connConnect,
|
connect: connConnect,
|
||||||
|
disconnect: connDisconnect,
|
||||||
sendPrompt,
|
sendPrompt,
|
||||||
setMode: connSetMode,
|
setMode: connSetMode,
|
||||||
setConfigOption: connSetConfigOption,
|
setConfigOption: connSetConfigOption,
|
||||||
@@ -256,9 +257,18 @@ export function useConnectionLifecycle({
|
|||||||
t,
|
t,
|
||||||
])
|
])
|
||||||
|
|
||||||
// Clean up lingering task on unmount (e.g. tab closed while connecting)
|
// Keep a ref to disconnect so the unmount cleanup always calls the
|
||||||
|
// latest version without adding it as a dependency.
|
||||||
|
const connDisconnectRef = useRef(connDisconnect)
|
||||||
|
useEffect(() => {
|
||||||
|
connDisconnectRef.current = connDisconnect
|
||||||
|
}, [connDisconnect])
|
||||||
|
|
||||||
|
// Clean up on unmount (e.g. tab closed): disconnect the ACP connection
|
||||||
|
// so it doesn't leak, and remove lingering tasks.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
connDisconnectRef.current().catch(() => {})
|
||||||
if (taskIdRef.current) {
|
if (taskIdRef.current) {
|
||||||
removeTask(taskIdRef.current)
|
removeTask(taskIdRef.current)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user