Skip to content

Fix Windows SessionEnd hang and taskkill false failures - #776

Open
Kenshiro787 wants to merge 2 commits into
openai:mainfrom
Kenshiro787:fix/windows-sessionend-taskkill
Open

Kenshiro787 wants to merge 2 commits into
openai:mainfrom
Kenshiro787:fix/windows-sessionend-taskkill

Conversation

@Kenshiro787

Copy link
Copy Markdown

Summary

Two Windows-specific problems in the plugin's process teardown, observed on Windows 11 with Node 26:

  1. SessionEnd hook never returned. sendBrokerShutdown waited forever when the broker accepted the connection but never replied, so Claude Code reported the hook as cancelled. It now gives up after a timeout (2 s by default) and destroys the socket.
  2. cancel failed although the task had been killed. terminateProcessTree treated any non-zero taskkill status as a failure. On Windows, taskkill /PID <root> /T /F enumerates the tree first and then terminates each entry; a short-lived descendant (git/cmd helpers spawned by the worker) that exits in between makes taskkill report "The operation attempted is not supported" and exit non-zero even though the root process was killed. This reproduced in 7 of 8 controlled runs and made the cancel integration test flaky (0/5 before the change, 3/3 after).

Changes

  • broker-lifecycle.mjs: sendBrokerShutdown(endpoint, timeoutMs = 2000) resolves on timeout and clears the timer on data/error/close.
  • process.mjs: on Windows, terminateProcessTree probes the root with process.kill(pid, 0) before running taskkill. A root that is already gone returns { attempted: false, delivered: false, method: null } without invoking taskkill. After a non-zero taskkill status, the root's liveness decides the outcome: root gone means delivered, root still alive keeps the previous behaviour and throws. The English-only looksLikeMissingProcessMessage filter is removed, so the result no longer depends on the localized taskkill message text (a French "introuvable" used to fall through and be reported as a successful kill). Comments state that delivered describes the root only, like the process-group SIGTERM on other platforms; it does not prove that every descendant is gone. Non-Windows code paths and the ENOENT fallback are unchanged.
  • broker-endpoint.mjs: the unix endpoint path uses path.posix.join, so it stays a valid socket path when computed on Windows.
  • Test fixtures on Windows: skip the broken-symlink case when the runner cannot create symlinks (no Developer Mode), use a node.cmd shim instead of a symlink, and propagate USERPROFILE next to HOME.

Tests

  • New: sendBrokerShutdown times out against a mute broker; terminateProcessTree with localized taskkill output and liveness probes, taskkill skipped when the root is already absent, EPERM on the probe not treated as absent, ENOENT fallback preserved, non-Windows path unchanged. Against the previous process.mjs, 4 of the 8 process tests fail.
  • Full suite on Windows 11 / Node 26.1.0, run from native cmd.exe: 98 tests, 97 passed, 0 failed, 1 skipped (the symlink fixture without Developer Mode). The cancel integration test passed 3/3 consecutive runs.

Known limitations

  • delivered is root-scoped. A descendant that taskkill could not stop after the root died is not detected, which matches the process-group signal semantics on other platforms; none of the callers reads the return value.
  • Narrow TOCTOU windows remain (root exiting on its own between the probe and taskkill; PID reuse before the second probe). They only affect the reported status, not the teardown itself.

🤖 Generated with Claude Code

Kenshiro787 and others added 2 commits September 21, 2026 00:00
sendBrokerShutdown waited forever when the broker accepted the
connection but never replied, so the SessionEnd hook never returned
(observed as "Hook cancelled"). It now gives up after a timeout and
destroys the socket; a regression test covers a mute broker.

terminateProcessTree treated any non-zero taskkill status as a failure.
On Windows, taskkill /T enumerates the tree and then terminates each
entry; a short-lived descendant (git/cmd helpers spawned by the worker)
that exits in between makes taskkill report "The operation attempted is
not supported" with status 128 even though the root process was killed.
The root's liveness is now checked before treating that as an error,
which fixes the flaky cancel integration test (reproduced 7/8 runs).

Also make the unix endpoint path POSIX-joined and adapt Windows test
fixtures: skip the broken-symlink case without Developer Mode, use a
node.cmd shim instead of a symlink, and propagate USERPROFILE.

Full suite on Windows 11 / Node 26.1.0: 93 passed, 0 failed, 1 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review of the previous commit found that terminateProcessTree could
report delivered: true for a Windows process that was already gone
before the call: looksLikeMissingProcessMessage only recognised English
taskkill messages, so on a localised system (French "introuvable") the
"not found" status fell through to the liveness check and was taken as
a successful kill.

terminateProcessTree now probes the root with process.kill(pid, 0)
before running taskkill. A root that is already absent returns
{ attempted: false, delivered: false, method: null } without invoking
taskkill, in every system language; looksLikeMissingProcessMessage and
the text-based branch are removed. EPERM on the probe still means the
process exists, so taskkill runs as before. The post-taskkill liveness
check and the ENOENT fallback are unchanged.

Comments now state the scope of delivered: it describes the root only,
like the process-group SIGTERM on other platforms, and does not prove
that every descendant is gone. Narrow TOCTOU windows (root exiting on
its own between probe and taskkill, PID reuse before the second probe)
are accepted; none of the callers reads the return value.

Tests: French taskkill output with liveness probes, taskkill skipped
when the root is already absent, EPERM on the probe not treated as
absent, ENOENT fallback preserved, non-Windows path unchanged; existing
mocks now lock the killImpl(pid, 0) call. Against the previous
process.mjs, 4 of the 8 tests fail.

Windows 11 / Node 26.1.0, run from native cmd.exe: process.test.mjs
8 passed; cancel integration test 3/3; full suite 98 tests, 97 passed,
0 failed, 1 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Kenshiro787
Kenshiro787 requested a review from a team September 21, 2026 01:36
Edo771977 added a commit to Edo771977/codex-plugin-cc that referenced this pull request Sep 22, 2026
Import openai#776's teardown half: decide on the root's liveness, not on taskkill's message
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant