Skip to content

fix(hooks): exit the SessionEnd hook within Claude Code's shutdown grace#992

Open
costajohnt wants to merge 1 commit into
rohitg00:mainfrom
costajohnt:fix/991-session-end-hook-cancelled
Open

fix(hooks): exit the SessionEnd hook within Claude Code's shutdown grace#992
costajohnt wants to merge 1 commit into
rohitg00:mainfrom
costajohnt:fix/991-session-end-hook-cancelled

Conversation

@costajohnt

@costajohnt costajohnt commented Jun 29, 2026

Copy link
Copy Markdown

What

Fixes #991 — the SessionEnd hook printed "Hook cancelled" on exit.

Root cause

src/hooks/session-end.ts deferred its exit with setTimeout(() => process.exit(0), 1500).unref(). The hook fires a fire-and-forget POST to the local memory server; while that request is in flight it keeps the process alive, so the process doesn't exit until the 1500ms timer fires. That overruns Claude Code's SessionEnd shutdown grace, so the harness kills the hook and reports "Hook cancelled".

The other seven lightweight hooks already use a 500ms cap; only session-end and stop were outliers at 1500ms.

Fix

Lower the cap to 500ms (source + the committed plugin/scripts/session-end.mjs artifact). 500ms is still ample to flush the small POST to the local server, and keeps the hook comfortably inside the shutdown grace.

Test

test/session-end-exit-timing.test.ts runs the built hook against a server that accepts the connection but never responds (so the request hangs and the deferred-exit timer is what ends the process), then asserts it exits well under the old 1500ms cap. Fails before this change (~1580ms), passes after (~580ms). Full suite: 1416 tests pass.

Note: stop.ts

src/hooks/stop.ts has the identical 1500ms deferred exit and likely shows the same symptom. I kept this PR scoped to the reported SessionEnd issue, but happy to fix stop too (here or a follow-up) if you'd like — let me know if 1500ms was intentional there.

Summary by CodeRabbit

  • Bug Fixes
    • Reduced the delay before session shutdown completes, so the app exits sooner after the session ends.
  • Tests
    • Added coverage to verify session-end shutdown timing when a request hangs, helping prevent regressions in exit behavior.

…ace (rohitg00#991)

The SessionEnd hook deferred its exit by 1500ms via
`setTimeout(() => process.exit(0), 1500).unref()`. When the memory server is
slow to respond, the in-flight request keeps the process alive until that timer
fires, which overruns Claude Code's SessionEnd shutdown grace -- the harness
then kills the hook and prints "Hook cancelled".

Lower the cap to 500ms, matching the seven other lightweight hooks (only
session-end and stop shared the 1500ms outlier). 500ms is still ample to flush
the fire-and-forget POST to the local memory server.

Add a test that runs the built hook against a server that never responds and
asserts it exits well inside the grace window.
@vercel

vercel Bot commented Jun 29, 2026

Copy link
Copy Markdown

@costajohnt is attempting to deploy a commit to the rohitg00's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The session-end hook's deferred process.exit(0) timeout is reduced from 1500ms to 500ms in both src/hooks/session-end.ts and the built plugin/scripts/session-end.mjs. A new Vitest integration test is added that verifies the hook exits within Claude Code's shutdown grace window even when outbound HTTP requests never respond.

Session-end exit timing fix

Layer / File(s) Summary
Reduce process.exit delay to 500ms
src/hooks/session-end.ts, plugin/scripts/session-end.mjs
setTimeout(() => process.exit(0), ...) delay changed from 1500ms to 500ms in both the TypeScript source and the built script.
Integration test for exit timing under hung requests
test/session-end-exit-timing.test.ts
Adds a test harness with a black-hole HTTP server and a child-process runner that spawns the built hook, measures elapsed time, and asserts exit code 0 and a runtime below the grace-window ceiling.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~5 minutes

Possibly related PRs

  • rohitg00/agentmemory#688: Introduced the fire-and-forget multi-request shutdown timing in session-end that this PR directly adjusts.

Poem

🐇 Five hundred milliseconds, that's all I need,
To fire my POST and exit with speed!
No more "Hook cancelled" to ruin my day,
I slip out the door before they say nay.
A tidy goodbye, then I hop away~ 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states the main change: reducing SessionEnd exit timing to avoid shutdown-grace cancellations.
Linked Issues check ✅ Passed The timeout is reduced to 500ms in both source and built hook, and the added test validates the fix against the reported cancellation issue.
Out of Scope Changes check ✅ Passed The added test and built artifact update directly support the shutdown-grace fix; no unrelated changes are present.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/hooks/session-end.ts`:
- Line 66: The `session-end` hook is unconditionally forcing process exit after
500ms, which is too short when it fans out to the extra fire-and-forget POSTs.
Update the exit timing in `src/hooks/session-end.ts` so the `setTimeout(() =>
process.exit(0), ...)` delay stays at the longer 1500ms cap whenever the
additional request paths in `session-end` are enabled, and only use a shorter
delay for the single-request path if needed. Use the existing `session-end` flow
around the `/session/end` call and the multi-POST logic to make the timeout
conditional instead of shrinking every run.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 83fff74b-4140-4f95-99b5-a9559072a32b

📥 Commits

Reviewing files that changed from the base of the PR and between 93ae9bc and 13a2f3b.

📒 Files selected for processing (3)
  • plugin/scripts/session-end.mjs
  • src/hooks/session-end.ts
  • test/session-end-exit-timing.test.ts

Comment thread src/hooks/session-end.ts
}

setTimeout(() => process.exit(0), 1500).unref();
setTimeout(() => process.exit(0), 500).unref();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep the longer exit cap when session-end fans out to multiple requests.

This hook always sends /session/end, and lines 42-64 can enqueue up to three more fire-and-forget POSTs. With those paths enabled, forcing exit after 500ms can cut the process before the later requests have flushed, so session-end can silently drop consolidation / bridge work. If you want the single-request path to stay under Claude Code’s grace window, make the delay conditional on whether the extra POSTs are enabled instead of shrinking every session-end run.

Suggested change
-  setTimeout(() => process.exit(0), 500).unref();
+  const exitDelayMs =
+    process.env["CONSOLIDATION_ENABLED"] === "true" ||
+    process.env["CLAUDE_MEMORY_BRIDGE"] === "true"
+      ? 1500
+      : 500;
+
+  setTimeout(() => process.exit(0), exitDelayMs).unref();

As per the hook contract in AGENTS.md, multi-request telemetry hooks like session-end should keep the 1500ms cap so all fire-and-forget requests have time to start.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setTimeout(() => process.exit(0), 500).unref();
const exitDelayMs =
process.env["CONSOLIDATION_ENABLED"] === "true" ||
process.env["CLAUDE_MEMORY_BRIDGE"] === "true"
? 1500
: 500;
setTimeout(() => process.exit(0), exitDelayMs).unref();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/session-end.ts` at line 66, The `session-end` hook is
unconditionally forcing process exit after 500ms, which is too short when it
fans out to the extra fire-and-forget POSTs. Update the exit timing in
`src/hooks/session-end.ts` so the `setTimeout(() => process.exit(0), ...)` delay
stays at the longer 1500ms cap whenever the additional request paths in
`session-end` are enabled, and only use a shorter delay for the single-request
path if needed. Use the existing `session-end` flow around the `/session/end`
call and the multi-POST logic to make the timeout conditional instead of
shrinking every run.

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.

SessionEnd hook reports "Hook cancelled" on exit (1.5s deferred exit outlives shutdown grace)

1 participant