fix(core): address memory leaks in oauth flow, chat history, and shell parsing

This commit is contained in:
Spencer Tang
2026-04-08 16:25:08 -04:00
parent 4ebc43bc66
commit 200d240dc9
5 changed files with 36 additions and 11 deletions
+5 -5
View File
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { useEffect, useReducer, useRef } from 'react';
import { useEffect, useReducer, useRef, useCallback } from 'react';
import { setTimeout as setTimeoutPromise } from 'node:timers/promises';
import * as path from 'node:path';
import {
@@ -224,15 +224,15 @@ export function useAtCompletion(props: UseAtCompletionProps): void {
setIsLoadingSuggestions(state.isLoading);
}, [state.isLoading, setIsLoadingSuggestions]);
const resetFileSearchState = () => {
const resetFileSearchState = useCallback(() => {
fileSearchMap.current.clear();
initEpoch.current += 1;
dispatch({ type: 'RESET' });
};
}, []);
useEffect(() => {
resetFileSearchState();
}, [cwd, config]);
}, [cwd, config, resetFileSearchState]);
useEffect(() => {
const workspaceContext = config?.getWorkspaceContext?.();
@@ -242,7 +242,7 @@ export function useAtCompletion(props: UseAtCompletionProps): void {
workspaceContext.onDirectoriesChanged(resetFileSearchState);
return unsubscribe;
}, [config]);
}, [config, resetFileSearchState]);
// Reacts to user input (`pattern`) ONLY.
useEffect(() => {
@@ -83,7 +83,12 @@ export function useHistory({
return prevHistory; // Don't add the duplicate
}
}
return [...prevHistory, newItem];
const newHistory = [...prevHistory, newItem];
// Enforce a hard limit of 1000 items to prevent unbounded memory growth
if (newHistory.length > 1000) {
return newHistory.slice(newHistory.length - 1000);
}
return newHistory;
});
// Record UI-specific messages, but don't do it if we're actually loading
+4 -2
View File
@@ -424,6 +424,7 @@ async function authWithUserCode(client: OAuth2Client): Promise<boolean> {
'\n\n',
);
let authTimeoutId: NodeJS.Timeout | undefined;
const code = await new Promise<string>((resolve, reject) => {
const rl = readline.createInterface({
input: process.stdin,
@@ -431,7 +432,7 @@ async function authWithUserCode(client: OAuth2Client): Promise<boolean> {
terminal: true,
});
const timeout = setTimeout(() => {
authTimeoutId = setTimeout(() => {
rl.close();
reject(
new FatalAuthenticationError(
@@ -441,10 +442,11 @@ async function authWithUserCode(client: OAuth2Client): Promise<boolean> {
}, 300000); // 5 minute timeout
rl.question('Enter the authorization code: ', (code) => {
clearTimeout(timeout);
rl.close();
resolve(code.trim());
});
}).finally(() => {
if (authTimeoutId) clearTimeout(authTimeoutId);
});
if (!code) {
+11 -2
View File
@@ -116,6 +116,8 @@ export function startCallbackServer(
portReject = reject;
});
let timeoutId: NodeJS.Timeout | undefined;
const responsePromise = new Promise<OAuthAuthorizationResponse>(
(resolve, reject) => {
let serverPort: number;
@@ -222,7 +224,7 @@ export function startCallbackServer(
});
// Timeout after 5 minutes
setTimeout(
timeoutId = setTimeout(
() => {
server.close();
reject(new Error('OAuth callback timeout'));
@@ -232,7 +234,14 @@ export function startCallbackServer(
},
);
return { port: portPromise, response: responsePromise };
return {
port: portPromise,
response: responsePromise.finally(() => {
if (timeoutId) {
clearTimeout(timeoutId);
}
}),
};
}
/**
+10 -1
View File
@@ -164,7 +164,16 @@ async function loadBashLanguage(): Promise<void> {
export async function initializeShellParsers(): Promise<void> {
if (!treeSitterInitialization) {
treeSitterInitialization = loadBashLanguage().catch((error) => {
const timeoutPromise = new Promise<void>((_, reject) => {
setTimeout(
() => reject(new Error('Tree-sitter initialization timed out')),
5000,
);
});
treeSitterInitialization = Promise.race([
loadBashLanguage(),
timeoutPromise,
]).catch((error) => {
treeSitterInitialization = null;
// Log the error but don't throw, allowing the application to fall back to safe defaults (ASK_USER)
// or regex checks where appropriate.