Skip to content

fix(routing): scope guidance throttle by session, not process.ppid (#298)#313

Merged
mksglu merged 1 commit into
mksglu:nextfrom
ousamabenyounes:fix/issue-298
Apr 25, 2026
Merged

fix(routing): scope guidance throttle by session, not process.ppid (#298)#313
mksglu merged 1 commit into
mksglu:nextfrom
ousamabenyounes:fix/issue-298

Conversation

@ousamabenyounes
Copy link
Copy Markdown
Contributor

@ousamabenyounes ousamabenyounes commented Apr 20, 2026

What

guidanceOnce() in hooks/core/routing.mjs scoped its on-disk marker directory by process.ppid. On Windows + Git Bash each hook invocation spawns a fresh bash.exe with a different PID, so the marker directory name changed every call, the throttle never fired, and Read/Bash/Grep guidance was injected on every tool use — 716 stale marker dirs in /tmp and ~10 KB of repetitive context noise per session, per #298.

This PR threads the stable sessionId from the hook payload through routePreToolUse and names the marker directory context-mode-guidance-s-<sessionId> instead. The legacy ppid-based path stays as a backward-compatible fallback for callers that don't pass a session identifier.

Fixes #298.

Why sessionId, not just a patched ppid

  • Every adapter hook (claude-code, codex, cursor, gemini-cli, kiro, vscode-copilot) already reads hook input and has getSessionId(input, <PLATFORM_OPTS>) available via session-helpers.mjs. It's derived from the hook payload's transcript_path / conversation_id / sessionId / session_id / env var, with a final pid-${ppid} fallback. That identifier is stable across hook invocations within the same logical session.
  • Passing it through routePreToolUse is additive (optional 5th arg) — no behavior change for callers that don't pass one, and no need to rely on platform-specific env vars or extra syscalls.
  • The alternatives from the issue (grandparent PID, writing a session marker from sessionstart.mjs) all required more moving parts to reach the same fix.

Changes

hooks/core/routing.mjs

  • _guidanceId is no longer pinned at module load — guidanceDirFor(sessionId) resolves the marker dir per call, using s-<sessionId> when one is passed, falling back to the ppid suffix otherwise.
  • guidanceOnce(type, content, sessionId?) and resetGuidanceThrottle(sessionId?) accept the optional session id.
  • routePreToolUse(toolName, toolInput, projectDir, platform, sessionId?) threads it into the three guidanceOnce call sites (Bash/Read/Grep).

Six adapter hooks now import the matching *_OPTS (or none for Ora Studio default) and pass getSessionId(input, ...) at the routing call site:

  • hooks/pretooluse.mjs (claude-code)
  • hooks/codex/pretooluse.mjs
  • hooks/cursor/pretooluse.mjs
  • hooks/gemini-cli/beforetool.mjs
  • hooks/kiro/pretooluse.mjs
  • hooks/vscode-copilot/pretooluse.mjs

Scope note: src/openclaw-plugin.ts and src/opencode-plugin.ts are intentionally not touched. Both run in-process across hook invocations, so process.ppid was never unstable for them and the ppid fallback stays correct.

Coverage added (added to tests/guidance-throttle.test.ts, per CONTRIBUTING's "no new test files" rule)

5 regression cases in a new describe("sessionId scoping (#298 — stable across shifting ppids)"):

  • second call with same sessionId is throttled even when in-memory Set is cleared — simulates the Windows Git Bash case (fresh process per hook) and asserts the on-disk marker still blocks the second call.
  • different sessionIds get independent throttles — two sessions, same process, each fires its own guidance exactly once.
  • sessionId routing is immune to process.ppid changes — plants a marker in the legacy ppid dir; the sessionId-scoped call ignores it.
  • resetGuidanceThrottle(sessionId) clears the session-scoped dir.
  • no sessionId passed → falls back to ppid-based behavior (backward compat) — the previously existing contract is preserved for in-process callers.

Subprocess-based tests (tests/hooks/integration.test.ts, tests/hooks/vscode-hooks.test.ts) now also clean context-mode-guidance-s-pid-<testpid> in their beforeEach so the new marker location doesn't leak between tests.

Test plan

  • npx vitest run tests/guidance-throttle.test.ts → 13/13 pass (5 new + 8 existing)
  • npx vitest run tests/hooks/integration.test.ts tests/hooks/vscode-hooks.test.ts tests/guidance-throttle.test.ts → 67/67 pass
  • npm test → 1600 pass, 23 skipped, 0 failures — including the subprocess JSON-parse cases in integration.test.ts that were flaking on next
  • npm run typecheck → clean

No runtime behavior changes for macOS/Linux users. The fix activates when a caller passes a session id — which all six adapter hooks now do.

Generated by Ora Studio
Vibe coded by ousamabenyounes

…ksglu#298)

On Windows + Git Bash each hook invocation spawns a fresh bash.exe with a
different PID, so the legacy ppid-based marker directory changed every
call and guidance was injected on every tool use instead of once per
session. The PreToolUse hooks already have a stable session identifier
in the hook payload; this change plumbs it into `routePreToolUse` and
names the marker directory `context-mode-guidance-s-<sessionId>`. The
ppid path stays as a backward-compatible fallback for callers that
don't pass one.

- core/routing.mjs: guidanceOnce(type, content, sessionId?), marker dir
  resolved per-call instead of pinned at module load.
- All six adapter hooks (claude-code, codex, cursor, gemini-cli, kiro,
  vscode-copilot) now pass getSessionId(input, <PLATFORM_OPTS>).
- In-process plugins (openclaw, opencode) are unchanged — they share
  a Node process across hook invocations, so process.ppid was never
  unstable for them.

Tests: 5 new regression cases in tests/guidance-throttle.test.ts prove
the throttle is now immune to ppid churn, that distinct sessionIds get
independent throttles, and that the legacy no-sessionId path still
works. Subprocess-based integration tests (integration.test.ts,
vscode-hooks.test.ts) updated to clean the new session-scoped dir.

- npx vitest run tests/guidance-throttle.test.ts → 13/13 pass
- npm test → 1600/1600 pass, 23 skipped, 0 failures
- npm run typecheck → clean

Co-Authored-By: Claude <noreply@anthropic.com>
@mksglu mksglu merged commit 4f6f7cb into mksglu:next Apr 25, 2026
5 checks passed
@mksglu
Copy link
Copy Markdown
Owner

mksglu commented Apr 25, 2026

Thanks @ousamabenyounes — threading the stable sessionId through routePreToolUse is the right approach. Clean backward-compat fallback, all 6 adapter hooks updated, solid test coverage. Merged!

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.

2 participants