fix(desktop): plug memory leaks causing 37GB+ RAM growth (#223)

This commit is contained in:
Wes
2026-04-03 12:17:51 -07:00
committed by GitHub
parent b2762d3253
commit 86be72a0c9
6 changed files with 35 additions and 4 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ const rules = [
const overrides = new Map([
["src-tauri/src/managed_agents/personas.rs", 600], // built-in persona system prompts (Solo, Ralph, Strategist) are long string literals
["src-tauri/src/managed_agents/persona_card.rs", 772], // PNG/ZIP persona card codec + provider/model fields + 27 unit tests (~350 lines of tests); rustfmt adds line breaks around long literals/builders
["src/app/AppShell.tsx", 840], // message edit state + handlers + ChannelPane edit prop threading + scrollback pagination + workflows view
["src/app/AppShell.tsx", 860], // message edit state + handlers + ChannelPane edit prop threading + scrollback pagination + workflows view + memory-leak safeguards
["src/features/channels/hooks.ts", 550], // canvas query + mutation hooks + DM hide mutation
["src/features/channels/ui/ChannelManagementSheet.tsx", 800],
["src/features/messages/hooks.ts", 500], // message query/mutation hooks + optimistic updates
+15
View File
@@ -483,6 +483,21 @@ export function AppShell() {
requestedAncestorIdsRef.current.add(eventId);
}
// Prevent unbounded growth — evict oldest entries when the set exceeds
// a reasonable size. Since Set iteration is insertion-ordered we drop
// the first (oldest) entries.
const MAX_REQUESTED_ANCESTORS = 500;
if (requestedAncestorIdsRef.current.size > MAX_REQUESTED_ANCESTORS) {
const excess =
requestedAncestorIdsRef.current.size - MAX_REQUESTED_ANCESTORS;
let removed = 0;
for (const id of requestedAncestorIdsRef.current) {
if (removed >= excess) break;
requestedAncestorIdsRef.current.delete(id);
removed++;
}
}
let isCancelled = false;
void Promise.all(
+1 -1
View File
@@ -140,7 +140,7 @@ export function useChannelMessagesQuery(channel: Channel | null) {
return mergedHistory;
},
staleTime: Number.POSITIVE_INFINITY,
gcTime: 30 * 60 * 1_000,
gcTime: 5 * 60 * 1_000,
});
}
@@ -373,6 +373,19 @@ export function useFeedDesktopNotifications(
for (const item of currentFeedItems) {
nextSeenItemIds.add(item.id);
}
// Prevent unbounded growth — keep only the most recent entries.
const MAX_SEEN_FEED_ITEMS = 500;
if (nextSeenItemIds.size > MAX_SEEN_FEED_ITEMS) {
const excess = nextSeenItemIds.size - MAX_SEEN_FEED_ITEMS;
let removed = 0;
for (const id of nextSeenItemIds) {
if (removed >= excess) break;
nextSeenItemIds.delete(id);
removed++;
}
}
seenItemIdsRef.current = nextSeenItemIds;
for (const item of newItems) {
+1
View File
@@ -14,6 +14,7 @@ const queryClient = new QueryClient({
retry: 1,
refetchOnWindowFocus: false,
networkMode: "always",
gcTime: 5 * 60 * 1_000,
},
mutations: {
networkMode: "always",
+4 -2
View File
@@ -44,6 +44,7 @@ export class RelayClient {
private reconnectListeners = new Set<() => void>();
private hasConnectedOnce = false;
private notifyReconnectListeners = false;
private onMessageChannel: Channel<unknown> | null = null;
async fetchChannelHistory(channelId: string, limit = 50) {
return this.fetchHistory(this.buildChannelFilter(channelId, limit));
@@ -217,13 +218,13 @@ export class RelayClient {
this.relayUrl = await getRelayWsUrl();
}
const onMessageChannel = new Channel<unknown>((message) => {
this.onMessageChannel = new Channel<unknown>((message) => {
void this.handleWsMessage(message);
});
this.wsId = await invoke<number>("plugin:websocket|connect", {
url: this.relayUrl,
onMessage: onMessageChannel,
onMessage: this.onMessageChannel,
config: {},
});
@@ -700,6 +701,7 @@ export class RelayClient {
reconnect?: boolean;
},
) {
this.onMessageChannel = null;
if (this.flushTimeout !== null) window.clearTimeout(this.flushTimeout);
this.flushTimeout = null;
this.eventBuffer = [];