Skip to content

fix(hooks): harden memory/hook helpers β€” real timeout, signal cleanup, truncation transparency, cross-platform slug#2389

Open
tjaiyen wants to merge 3 commits into
ruvnet:mainfrom
tjaiyen:harden-latest
Open

fix(hooks): harden memory/hook helpers β€” real timeout, signal cleanup, truncation transparency, cross-platform slug#2389
tjaiyen wants to merge 3 commits into
ruvnet:mainfrom
tjaiyen:harden-latest

Conversation

@tjaiyen

@tjaiyen tjaiyen commented Jun 15, 2026

Copy link
Copy Markdown

Summary

Five hardening fixes to the .claude/helpers hook/memory scripts, all reproduced against current main. Each is small, isolated, and verified with Node's built-in test runner (no new deps).

Fixes

  1. runWithTimeout() never enforced its timeout (hook-handler.cjs). It called fn() then clearTimeout(timer) immediately, so an async fn returned a pending promise that resolved through the race β€” the timeout protected nothing (the intelligence-init guard for Hooks causing ~20s latency on every Claude Code CLI interactionΒ #1530/[BUG] Intelligence hooks cause indefinite hang β€” PageRank on 150MB JSON blocks every CLI interactionΒ #1531 was inert). Reimplemented as a real Promise.race between the work and the timeout. Documented that a synchronous blocking callee can't be preempted in-process (the real guard there is the file-size cap in intelligence.cjs). Applied to both hook-handler.cjs copies (root + v3/@claude-flow/cli).

  2. Global silent swallow of unhandled rejections (auto-memory-hook.mjs). process.on('unhandledRejection', () => {}) hid all async failures process-wide. Kept exit-0 behaviour but log the reason under RUFLO_DEBUG/DEBUG so genuine bugs are visible.

  3. No SIGTERM/SIGINT cleanup in db-touching helpers (auto-memory-hook.mjs, context-persistence-hook.mjs). A signal mid-write skipped backend.shutdown(), risking an unflushed SQLite WAL / stale agentdb.rvf.lock. Track the active backend and flush it on signal.

  4. Silent value truncation (intelligence.cjs). Memory content was cut at 500 chars with no signal. Added a clip() helper that appends an ellipsis and warns under debug when it actually truncates.

  5. Cross-platform projectSlug bug (intelligence.cjs). The slug used cwd().replace(/^\//,'').replace(/\//g,'-') β€” POSIX-only. On Windows it kept : and \, so it never matched Claude Code's real ~/.claude/projects/<slug> dir and memory bootstrap silently found nothing. Now slugifies every non-alphanumeric to -, matching Claude Code's actual convention (verified: G:\My Drive\...\ruflo-fix -> G--My-Drive-...-ruflo-fix).

Testing

  • node --test tests/hook-handler-runwithtimeout.test.cjs β€” 5/5, including the decisive case: a slow async fn now resolves null at ~3s (old code returned the late value at ~5s).
  • node scripts/smoke-pre-bash-hook.mjs β€” passes for both dispatcher copies (no regression).
  • node --check on all 8 touched files.

πŸ€– Generated with Claude Code

tjaiyen and others added 3 commits June 15, 2026 12:01
… cross-platform slug)

FIX 1 β€” runWithTimeout was inert: it called fn() then clearTimeout()
immediately, so an async callee's hang was never caught (the timer was
cleared before the pending promise settled). Reimplement as a real
Promise.race between the work and the timeout. Document that a synchronous
blocking callee cannot be preempted in-process (the real guard is the
file-size cap in intelligence.cjs). Applied to both hook-handler.cjs copies.

FIX 2 β€” auto-memory-hook.mjs swallowed ALL unhandled rejections process-wide
via `() => {}`. Keep hooks exit-0 but log the reason under RUFLO_DEBUG/DEBUG
so genuine async bugs are visible.

FIX 3 β€” no SIGTERM/SIGINT cleanup in the db-touching helpers. Track the active
backend and flush it (JSON persist / SQLite close + WAL flush) on signal,
avoiding half-written stores and stale agentdb.rvf.lock.

FIX 4 β€” silent value truncation (intelligence.cjs 500/100 chars). Add clip():
appends an ellipsis and warns under debug when it actually cuts.

FIX 5 β€” projectSlug only handled POSIX '/', so on Windows it never matched
Claude Code's real ~/.claude/projects/<slug> dir and memory bootstrap silently
found nothing. Slugify every non-alphanumeric to '-' to match Claude's
convention (verified: G:\My Drive\...\ruflo-fix -> G--My-Drive-...-ruflo-fix).

Also: export runWithTimeout behind a require.main guard so it is unit-testable,
and add tests/hook-handler-runwithtimeout.test.cjs (node:test, 5 cases incl.
the decisive slow-async timeout case).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Apply the analogous fixes to the package-distributed copies under
v3/@claude-flow/cli/.claude/helpers so `ruflo init` in new projects ships the
fixed behaviour:
- intelligence.cjs: cross-platform projectSlug (FIX 5) + clip() truncation
  transparency on memory-content (FIX 4).
- auto-memory-hook.mjs: scoped/debug-gated unhandledRejection (FIX 2) +
  SIGTERM/SIGINT backend flush (FIX 3).

FIX 1 was already applied to this copy's hook-handler.cjs in the previous
commit. context-persistence-hook.mjs has only one (root) copy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code syncs via this fork; durable memory (MEMORY.md / Obsidian vault) syncs via
Google Drive; the .claude-flow local cache is rebuilt per device from that
Markdown (works on Windows now via FIX 5). No servers required.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tjaiyen tjaiyen requested a review from ruvnet as a code owner June 15, 2026 19:06
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