fix(agents): stop leaking session lock exit listeners (#65469)

* fix(agents): stop leaking session lock exit listeners

* Update src/agents/session-write-lock.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Vincent Koc
2026-04-12 18:22:12 +01:00
committed by GitHub
parent a5aceebc01
commit f00f0a9596
3 changed files with 26 additions and 4 deletions

View File

@@ -210,6 +210,7 @@ Docs: https://docs.openclaw.ai
- Heartbeat/scheduling: spread interval heartbeats across stable per-agent phases derived from gateway identity, so provider traffic is distributed more uniformly across the configured interval instead of clustering around startup-relative times. (#64560) Thanks @odysseus0.
- Config/media: accept `tools.media.asyncCompletion.directSend` in strict config validation so gateways no longer reject the generated-schema-backed async media completion setting at startup. (#63618) Thanks @qiziAI.
- Telegram/exec: preserve delayed exec completion routing for forum topics by pinning background exec completions to the topic where the run started even if the session route later drifts. (#64580) thanks @jalehman.
- Agents/locks: unregister the session write-lock `exit` cleanup handler during teardown so repeated lock lifecycle resets stop stacking process listeners in long-running gateway processes. (#65391) Thanks @adminfedres and @vincentkoc.
## 2026.4.9

View File

@@ -426,6 +426,20 @@ describe("acquireSessionWriteLock", () => {
await expect(fs.access(lockPath)).rejects.toThrow();
});
});
it("does not accumulate exit listeners across reset cycles", async () => {
const baselineExitListeners = process.listenerCount("exit");
await withTempSessionLockFile(async ({ sessionFile }) => {
for (let i = 0; i < 3; i += 1) {
const lock = await acquireSessionWriteLock({ sessionFile, timeoutMs: 500 });
await lock.release();
resetSessionWriteLockStateForTest();
expect(process.listenerCount("exit")).toBe(baselineExitListeners);
}
});
});
it("keeps other signal listeners registered", () => {
const keepAlive = () => {};
const originalKill = process.kill.bind(process);

View File

@@ -49,6 +49,7 @@ const MAX_LOCK_HOLD_MS = 2_147_000_000;
type CleanupState = {
registered: boolean;
exitHandler?: () => void;
cleanupHandlers: Map<CleanupSignal, () => void>;
};
@@ -72,6 +73,7 @@ function resolveCleanupState(): CleanupState {
if (!proc[CLEANUP_STATE_KEY]) {
proc[CLEANUP_STATE_KEY] = {
registered: false,
exitHandler: undefined,
cleanupHandlers: new Map<CleanupSignal, () => void>(),
};
}
@@ -254,12 +256,13 @@ function handleTerminationSignal(signal: CleanupSignal): void {
function registerCleanupHandlers(): void {
const cleanupState = resolveCleanupState();
if (!cleanupState.registered) {
cleanupState.registered = true;
cleanupState.registered = true;
if (!cleanupState.exitHandler) {
// Cleanup on normal exit and process.exit() calls
process.on("exit", () => {
cleanupState.exitHandler = () => {
releaseAllLocksSync();
});
};
process.on("exit", cleanupState.exitHandler);
}
ensureWatchdogStarted(DEFAULT_WATCHDOG_INTERVAL_MS);
@@ -281,6 +284,10 @@ function registerCleanupHandlers(): void {
function unregisterCleanupHandlers(): void {
const cleanupState = resolveCleanupState();
if (cleanupState.exitHandler) {
process.off("exit", cleanupState.exitHandler);
cleanupState.exitHandler = undefined;
}
for (const [signal, handler] of cleanupState.cleanupHandlers) {
process.off(signal, handler);
}