修复新会话可能泄漏问题
This commit is contained in:
@@ -964,7 +964,33 @@ export function ConversationDetailPanel() {
|
||||
})
|
||||
}, [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(() => {
|
||||
let cancelled = false
|
||||
let unlisten: (() => void | Promise<void>) | null = null
|
||||
@@ -975,9 +1001,8 @@ export function ConversationDetailPanel() {
|
||||
const payload = event.payload
|
||||
if (payload.type !== "turn_complete") return
|
||||
|
||||
const runtimeConversationId = getConversationIdByExternalId(
|
||||
payload.session_id
|
||||
)
|
||||
const runtimeConversationId =
|
||||
getConversationIdByExternalIdRef.current(payload.session_id)
|
||||
const summary = conversationsRef.current.find(
|
||||
(item) => item.external_id === payload.session_id
|
||||
)
|
||||
@@ -997,12 +1022,12 @@ export function ConversationDetailPanel() {
|
||||
if (isOpenInTabs) return
|
||||
|
||||
// Promote liveMessage + optimisticTurns to localTurns immediately
|
||||
runtimeCompleteTurn(matchedConversationId)
|
||||
runtimeCompleteTurnRef.current(matchedConversationId)
|
||||
|
||||
// If tab was closed while agent was responding, clean up now
|
||||
const session = getSession(matchedConversationId)
|
||||
const session = getSessionRef.current(matchedConversationId)
|
||||
if (session?.pendingCleanup) {
|
||||
runtimeRemoveConversation(matchedConversationId)
|
||||
runtimeRemoveConversationRef.current(matchedConversationId)
|
||||
}
|
||||
|
||||
// Update conversation status — use the DB summary (found by
|
||||
@@ -1013,7 +1038,7 @@ export function ConversationDetailPanel() {
|
||||
(matchedConversationId > 0 ? matchedConversationId : null)
|
||||
if (dbId && (!summary || summary.status === "in_progress")) {
|
||||
updateConversationStatus(dbId, "pending_review")
|
||||
.then(() => refreshConversations())
|
||||
.then(() => refreshConversationsRef.current())
|
||||
.catch((error: unknown) =>
|
||||
console.error(
|
||||
"[ConversationDetailPanel] background update status:",
|
||||
@@ -1044,13 +1069,7 @@ export function ConversationDetailPanel() {
|
||||
"ConversationDetailPanel.backgroundRefresh"
|
||||
)
|
||||
}
|
||||
}, [
|
||||
getConversationIdByExternalId,
|
||||
getSession,
|
||||
runtimeCompleteTurn,
|
||||
runtimeRemoveConversation,
|
||||
refreshConversations,
|
||||
])
|
||||
}, [])
|
||||
|
||||
const hasNoTabs = tabs.length === 0 && !activeTabId
|
||||
const activeConversationTab = useMemo(
|
||||
|
||||
@@ -1149,6 +1149,8 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
// Guard against concurrent connect() calls
|
||||
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 =
|
||||
| { kind: "none"; reason: "" }
|
||||
@@ -1772,6 +1774,14 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
await waitForListenerReady()
|
||||
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)
|
||||
lastActivityRef.current.set(contextKey, Date.now())
|
||||
dispatch({
|
||||
@@ -1800,6 +1810,7 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
||||
throw err
|
||||
} finally {
|
||||
connectingKeysRef.current.delete(contextKey)
|
||||
abandonedKeysRef.current.delete(contextKey)
|
||||
}
|
||||
},
|
||||
[
|
||||
@@ -1816,7 +1827,14 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
|
||||
const disconnect = useCallback(
|
||||
async (contextKey: string) => {
|
||||
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)
|
||||
reverseMapRef.current.delete(conn.connectionId)
|
||||
lastActivityRef.current.delete(contextKey)
|
||||
|
||||
@@ -55,6 +55,7 @@ export function useConnectionLifecycle({
|
||||
status,
|
||||
selectorsReady,
|
||||
connect: connConnect,
|
||||
disconnect: connDisconnect,
|
||||
sendPrompt,
|
||||
setMode: connSetMode,
|
||||
setConfigOption: connSetConfigOption,
|
||||
@@ -256,9 +257,18 @@ export function useConnectionLifecycle({
|
||||
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(() => {
|
||||
return () => {
|
||||
connDisconnectRef.current().catch(() => {})
|
||||
if (taskIdRef.current) {
|
||||
removeTask(taskIdRef.current)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user