fix: plug resource leaks and add hook command timeout

- LSP signal handlers: store refs, return unregister handle, call in stopAll()
- session-tools-store: add per-session deleteSessionTools(), wire into session.deleted
- executeHookCommand: add 30s timeout with SIGTERM→SIGKILL escalation
This commit is contained in:
Cole Leavitt
2026-02-21 15:42:47 -07:00
parent c1ee4c8650
commit 6aa1e96f9e
5 changed files with 418 additions and 352 deletions
+48 -22
View File
@@ -1,45 +1,71 @@
type ManagedClientForCleanup = {
client: {
stop: () => Promise<void>
}
}
stop: () => Promise<void>;
};
};
type ProcessCleanupOptions = {
getClients: () => IterableIterator<[string, ManagedClientForCleanup]>
clearClients: () => void
clearCleanupInterval: () => void
}
getClients: () => IterableIterator<[string, ManagedClientForCleanup]>;
clearClients: () => void;
clearCleanupInterval: () => void;
};
type RegisteredHandler = {
event: string;
listener: (...args: unknown[]) => void;
};
export type LspProcessCleanupHandle = {
unregister: () => void;
};
export function registerLspManagerProcessCleanup(options: ProcessCleanupOptions): LspProcessCleanupHandle {
const handlers: RegisteredHandler[] = [];
export function registerLspManagerProcessCleanup(options: ProcessCleanupOptions): void {
// Synchronous cleanup for 'exit' event (cannot await)
const syncCleanup = () => {
for (const [, managed] of options.getClients()) {
try {
// Fire-and-forget during sync exit - process is terminating
void managed.client.stop().catch(() => {})
void managed.client.stop().catch(() => {});
} catch {}
}
options.clearClients()
options.clearCleanupInterval()
}
options.clearClients();
options.clearCleanupInterval();
};
// Async cleanup for signal handlers - properly await all stops
const asyncCleanup = async () => {
const stopPromises: Promise<void>[] = []
const stopPromises: Promise<void>[] = [];
for (const [, managed] of options.getClients()) {
stopPromises.push(managed.client.stop().catch(() => {}))
stopPromises.push(managed.client.stop().catch(() => {}));
}
await Promise.allSettled(stopPromises)
options.clearClients()
options.clearCleanupInterval()
}
await Promise.allSettled(stopPromises);
options.clearClients();
options.clearCleanupInterval();
};
process.on("exit", syncCleanup)
const registerHandler = (event: string, listener: (...args: unknown[]) => void) => {
handlers.push({ event, listener });
process.on(event, listener);
};
registerHandler("exit", syncCleanup);
// Don't call process.exit() here; other handlers (background-agent manager) handle final exit.
process.on("SIGINT", () => void asyncCleanup().catch(() => {}))
process.on("SIGTERM", () => void asyncCleanup().catch(() => {}))
const signalCleanup = () => void asyncCleanup().catch(() => {});
registerHandler("SIGINT", signalCleanup);
registerHandler("SIGTERM", signalCleanup);
if (process.platform === "win32") {
process.on("SIGBREAK", () => void asyncCleanup().catch(() => {}))
registerHandler("SIGBREAK", signalCleanup);
}
return {
unregister: () => {
for (const { event, listener } of handlers) {
process.off(event, listener);
}
handlers.length = 0;
},
};
}