From fd240408b47a6ce2a657ddd09c65b3b07617c593 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 24 Oct 2026 20:09:42 -0700 Subject: [PATCH] fix(pty): let agents run exit hooks before an immediate local-provider close SIGKILLs them (#33367) Local (in-process) PTY provider: an immediate close now SIGTERMs descendants and waits up to the shutdown grace before SIGKILL (same as the daemon path since #22232), so Claude's SessionEnd hooks run. Refs #23865, #22039. Supersedes #24314. Co-authored-by: innocarpe <2222333+innocarpe@users.noreply.github.com> --- src/main/providers/local-pty-provider-shutdown.test.ts | 2 ++ src/main/providers/local-pty-termination.ts | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/providers/local-pty-provider-shutdown.test.ts b/src/main/providers/local-pty-provider-shutdown.test.ts index a3b4021411b..2fd971c0a4f 100644 --- a/src/main/providers/local-pty-provider-shutdown.test.ts +++ b/src/main/providers/local-pty-provider-shutdown.test.ts @@ -523,6 +523,8 @@ describe('LocalPtyProvider', () => { expect.any(Function), expect.objectContaining({ ownsRoot: expect.any(Function) }) ) + // Root group SIGKILL must wait for the descendant SIGTERM grace, or Claude skips SessionEnd. + expect(killWithDescendantSweepMock.mock.calls[0][2].awaitEscalation()).toBe(true) }) }) diff --git a/src/main/providers/local-pty-termination.ts b/src/main/providers/local-pty-termination.ts index 426a7aad346..4eace9ae35a 100644 --- a/src/main/providers/local-pty-termination.ts +++ b/src/main/providers/local-pty-termination.ts @@ -1,6 +1,7 @@ import type * as pty from 'node-pty' import { PhysicalExitTracker } from '../../shared/physical-exit-tracker' import { killWithDescendantSweep } from '../pty-descendant-termination' +import { terminateShutdownDescendants } from '../daemon/terminal-descendant-shutdown' import { forceKillPosixPtyProcessGroups } from '../pty/posix-pty-process-groups' import { terminatePtyJob } from '../windows/windows-pty-job' import { @@ -179,7 +180,10 @@ async function shutdownTrackedPty( // Typed agents also detach tool process groups; immediate close must snapshot before root exit. await killWithDescendantSweep(proc.pid, signalRoot, { ownsRoot: () => ptyProcesses.get(id) === proc, - terminateOwnedTree: () => terminatePtyJob(proc) + terminateOwnedTree: () => terminatePtyJob(proc), + terminateDescendants: terminateShutdownDescendants, + // Group SIGKILL must wait out the SIGTERM grace so agents can run exit hooks (Claude SessionEnd). + awaitEscalation: () => operation.immediate }) } else { signalRoot()