Skip to content

fix: resolve ConfigCache.has() stat inflation and hook system errors#582

Merged
Pedrovaleriolopez merged 5 commits intomainfrom
fix/bugfix-497-528-config-cache-hooks
Mar 11, 2026
Merged

fix: resolve ConfigCache.has() stat inflation and hook system errors#582
Pedrovaleriolopez merged 5 commits intomainfrom
fix/bugfix-497-528-config-cache-hooks

Conversation

@oalanicolas
Copy link
Collaborator

@oalanicolas oalanicolas commented Mar 11, 2026

Summary

Fixes two reported bugs with correct implementations:

Related PRs

Test plan

  • All 303 test suites pass (7690 tests)
  • npm run lint — 0 errors
  • hook-entry.test.js updated to match new behavior (process.exitCode, createSession mock)
  • config-cache has() no longer inflates stats when called
  • precompact-session-digest resolves runner path in both framework-dev and installed-project modes

Closes #497
Closes #528

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Ensure sessions are initialized on first prompt to prevent silent failures.
    • Improve cache existence checks to avoid side effects and perform expiration cleanup.
  • Refactor

    • Make shutdown/exit flow more graceful to allow stdout flush and natural process termination.
    • Change hook execution to spawn a detached runner process and improve runner resolution/early-exit behavior.
  • Tests

    • Update session-related mocks and tests to validate exitCode-based termination and new session behavior.
  • Chores

    • Update install manifest metadata (timestamps, hashes, sizes).

@vercel
Copy link

vercel bot commented Mar 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
aios-core Ready Ready Preview, Comment Mar 11, 2026 3:04pm

Request Review

@github-actions github-actions bot added area: agents Agent system related area: workflows Workflow system related squad mcp type: test Test coverage and quality area: core Core framework (.aios-core/core/) area: installer Installer and setup (packages/installer/) area: synapse SYNAPSE context engine area: cli CLI tools (bin/, packages/aios-pro-cli/) area: pro Pro features (pro/) area: health-check Health check system area: docs Documentation (docs/) area: devops CI/CD, GitHub Actions (.github/) labels Mar 11, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 11, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 11bd6bcf-b561-4d39-bb9a-1a2aaa043d37

📥 Commits

Reviewing files that changed from the base of the PR and between d1bb475 and 2b0a61c.

📒 Files selected for processing (8)
  • .aiox-core/core/config/config-cache.js
  • .aiox-core/core/synapse/runtime/hook-runtime.js
  • .aiox-core/data/entity-registry.yaml
  • .aiox-core/infrastructure/scripts/config-cache.js
  • .aiox-core/install-manifest.yaml
  • .claude/hooks/precompact-session-digest.cjs
  • .claude/hooks/synapse-engine.cjs
  • tests/synapse/hook-entry.test.js
🚧 Files skipped from review as they are similar to previous changes (4)
  • .aiox-core/core/synapse/runtime/hook-runtime.js
  • tests/synapse/hook-entry.test.js
  • .aiox-core/infrastructure/scripts/config-cache.js
  • .claude/hooks/synapse-engine.cjs

Walkthrough

ConfigCache.has() now checks presence and TTL inline without calling get(); hook runtime will create a session if loadSession returns null; precompact hook resolves runner path and spawns a detached child process (or exits silently if missing); synapse-engine introduces a run() entrypoint and uses exitCode/timeouts; manifest hashes/timestamp updated.

Changes

Cohort / File(s) Summary
ConfigCache has()
./aiox-core/core/config/config-cache.js, ./aiox-core/infrastructure/scripts/config-cache.js
Reimplemented has(key) to check Map presence and TTL directly, evict expired entries inline, and avoid calling get() (prevents metric side effects).
Session initialization (hook runtime)
./aiox-core/core/synapse/runtime/hook-runtime.js
resolveHookRuntime now imports createSession, attempts loadSession, and calls createSession when loadSession returns null but sessionId exists; falls back to a default session object to ensure updates can write.
Precompact hook runner
.claude/hooks/precompact-session-digest.cjs
Added resolveRunnerPath to locate precompact-runner.js; if missing, exit silently. Replaced in-process require with spawning a detached child process, passing context via AIOX_HOOK_CONTEXT.
Synapse engine exit handling
.claude/hooks/synapse-engine.cjs
Removed safeExit, added exported run() that manages a 5s timeout and sets process.exitCode, allowing Node to exit naturally after stdout flush.
Tests: hook entry
tests/synapse/hook-entry.test.js
Test session mock now exports createSession alongside loadSession; tests assert process.exitCode === 0 instead of spying on process.exit.
Install manifest
./aiox-core/install-manifest.yaml
Updated generated_at, file_count, and refreshed hashes/sizes across many entries to reflect content changes.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Hook as "precompact-session-digest.cjs\n(Hook)"
participant FS as "Filesystem\n(resolveRunnerPath)"
participant Child as "Detached Child\n(node process)"
participant Runner as "precompact-runner.js\n(runner module)"
Hook->>FS: resolve candidate paths
alt runner found
FS-->>Hook: path
Hook->>Child: spawn detached process with AIOX_HOOK_CONTEXT (context JSON)
Note right of Child: runs independently
Child->>Runner: require(resolved path) and call onPreCompact(ctx)
Runner-->>Child: completes (fire-and-forget)
Child-->>Hook: (no blocking response)
else runner missing
FS-->>Hook: not found
Hook->>Hook: exit silently (no error)
end

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • Pedrovaleriolopez
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ❓ Inconclusive Additional changes to synapse-engine.cjs and hook-runtime.js address related issues from the broader hook system errors scope, but some reviewer feedback requests (detached process spawning, timeout enforcement) appear incomplete. Verify that reviewer-requested changes (detached spawn in precompact-session-digest.cjs and process.exit(0) in synapse-engine.cjs timeout callback) have been fully implemented before merge.
✅ 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 accurately summarizes the two main fixes: ConfigCache.has() stat inflation (issue #497) and hook system errors (issue #528).
Linked Issues check ✅ Passed The PR successfully addresses both linked issues: has() now checks the Map directly without calling get(), and precompact-session-digest is made resilient with multi-path runner resolution.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/bugfix-497-528-config-cache-hooks

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 and usage tips.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 11, 2026

📊 Coverage Report

Coverage report not available

📈 Full coverage report available in Codecov


Generated by PR Automation (Story 6.1)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
.aiox-core/core/config/config-cache.js (1)

74-85: Extract the expiry/eviction check into one helper.

get(), has(), and clearExpired() now all carry their own TTL logic here, and the same behavior is mirrored again in .aiox-core/infrastructure/scripts/config-cache.js. Centralizing the “is entry still valid, otherwise evict it” path would reduce drift on this core module.

♻️ Possible refactor
+  _hasLiveEntry(key, now = Date.now()) {
+    if (!this.cache.has(key)) {
+      return false;
+    }
+
+    const timestamp = this.timestamps.get(key);
+    if (now - timestamp > this.ttl) {
+      this.cache.delete(key);
+      this.timestamps.delete(key);
+      return false;
+    }
+
+    return true;
+  }
+
   get(key) {
-    if (!this.cache.has(key)) {
+    if (!this._hasLiveEntry(key)) {
       this.misses++;
       return null;
     }
-
-    const timestamp = this.timestamps.get(key);
-    const now = Date.now();
-
-    // Check if expired
-    if (now - timestamp > this.ttl) {
-      this.cache.delete(key);
-      this.timestamps.delete(key);
-      this.misses++;
-      return null;
-    }
 
     this.hits++;
     return this.cache.get(key);
   }
 
   has(key) {
-    if (!this.cache.has(key)) {
-      return false;
-    }
-    const timestamp = this.timestamps.get(key);
-    if (Date.now() - timestamp > this.ttl) {
-      this.cache.delete(key);
-      this.timestamps.delete(key);
-      return false;
-    }
-    return true;
+    return this._hasLiveEntry(key);
   }

As per coding guidelines, "Ensure backwards compatibility — core modules are consumed by all agents."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.aiox-core/core/config/config-cache.js around lines 74 - 85, The
TTL/eviction logic is duplicated across has(), get(), and clearExpired() (and
mirrored in the script version); extract that logic into a single helper method
(e.g., isExpiredAndEvict(key) or ensureNotExpired(key)) that checks
timestamps.get(key) vs Date.now(), deletes from this.cache and this.timestamps
when expired, and returns a boolean indicating validity; update has(), get(),
and clearExpired() to call this helper so behavior remains identical and keep
method name/signature stable to preserve backwards compatibility.
tests/synapse/hook-entry.test.js (1)

337-343: Please cover the successful createSession() path too.

These assertions only exercise loadSession() === null plus createSession() === null, so the new happy path in resolveHookRuntime() is still untested. Add one case where createSession() returns a real session object and mirror it in the in-process variant below.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/synapse/hook-entry.test.js` around lines 337 - 343, Add a new test case
(or extend the existing one) so that when loadSession() returns null but
createSession() returns a real session object, resolveHookRuntime() follows the
happy path and returns/uses that session; update both the existing in-process
test and its in-process variant to mock createSession() to return a real session
object (e.g., an object with prompt_count and any other expected fields) and add
assertions that the resolved session equals that object and that any derived
values (such as prompt_count) match; target the functions loadSession,
createSession, and resolveHookRuntime in tests/synapse/hook-entry.test.js to
ensure coverage of the successful createSession path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.claude/hooks/precompact-session-digest.cjs:
- Around line 73-75: The hook currently calls resolveRunnerPath() then executes
the runner which uses setImmediate(async => ...) in-process, keeping the event
loop alive; instead spawn a detached child process to run
.aiox-core/hooks/unified/runners/precompact-runner.js so the digest is
fire-and-forget. Modify the code that uses resolveRunnerPath() / runnerPath to
fork or spawn a detached process (child_process.fork or spawn) with the runner
path as the module/entry, set detached: true, stdio: 'ignore', and call
child.unref() so the parent can exit immediately; ensure any args or env needed
by the runner are forwarded and preserve the existing early return when
!runnerPath.

In @.claude/hooks/synapse-engine.cjs:
- Around line 90-94: The timeout callback in run() currently sets
process.exitCode = 0 which doesn't force termination; change the callback to
call process.exit(0) so the 5s limit is enforced (i.e., replace the body of the
setTimeout in run() to invoke process.exit(0)); keep the existing timer handling
(including timer.unref() if present) but ensure the timeout uses process.exit(0)
to forcibly terminate when HOOK_TIMEOUT_MS elapses.

---

Nitpick comments:
In @.aiox-core/core/config/config-cache.js:
- Around line 74-85: The TTL/eviction logic is duplicated across has(), get(),
and clearExpired() (and mirrored in the script version); extract that logic into
a single helper method (e.g., isExpiredAndEvict(key) or ensureNotExpired(key))
that checks timestamps.get(key) vs Date.now(), deletes from this.cache and
this.timestamps when expired, and returns a boolean indicating validity; update
has(), get(), and clearExpired() to call this helper so behavior remains
identical and keep method name/signature stable to preserve backwards
compatibility.

In `@tests/synapse/hook-entry.test.js`:
- Around line 337-343: Add a new test case (or extend the existing one) so that
when loadSession() returns null but createSession() returns a real session
object, resolveHookRuntime() follows the happy path and returns/uses that
session; update both the existing in-process test and its in-process variant to
mock createSession() to return a real session object (e.g., an object with
prompt_count and any other expected fields) and add assertions that the resolved
session equals that object and that any derived values (such as prompt_count)
match; target the functions loadSession, createSession, and resolveHookRuntime
in tests/synapse/hook-entry.test.js to ensure coverage of the successful
createSession path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 184d4eef-e5d1-4164-8b21-7711fa2e342b

📥 Commits

Reviewing files that changed from the base of the PR and between ff711c1 and 3b7c87d.

📒 Files selected for processing (7)
  • .aiox-core/core/config/config-cache.js
  • .aiox-core/core/synapse/runtime/hook-runtime.js
  • .aiox-core/infrastructure/scripts/config-cache.js
  • .aiox-core/install-manifest.yaml
  • .claude/hooks/precompact-session-digest.cjs
  • .claude/hooks/synapse-engine.cjs
  • tests/synapse/hook-entry.test.js

@nikolasdehor
Copy link
Contributor

Heads up @Pedrovaleriolopez — esse fix do ConfigCache.has() já está no nosso PR #498 (aberto 24/fev, há mais de 2 semanas). Mesmo bug, mesma solução. O #498 já tá rebased e pronto pra review.

@Pedrovaleriolopez
Copy link
Contributor

Hey @oalanicolas — CodeRabbit flagged 2 actionable issues + 2 nitpicks:

Actionable:

  1. .claude/hooks/precompact-session-digest.cjs (L73-75): Spawn a detached child process (child_process.fork with detached: true, stdio: 'ignore' + child.unref()) instead of running the runner in-process with setImmediate. Fire-and-forget pattern.
  2. .claude/hooks/synapse-engine.cjs (L90-94): Replace process.exitCode = 0 with process.exit(0) in the timeout callback so the 5s limit is actually enforced.

Nitpick (nice-to-have):
3. .aiox-core/core/config/config-cache.js (L74-85): Extract TTL/eviction check into a _hasLiveEntry() helper — duplicated across get(), has(), and clearExpired().
4. tests/synapse/hook-entry.test.js (L337-343): Add happy-path test for createSession() returning a real session object.

Please apply fixes and push. CI is green, just need CR approval to unblock merge.

oalanicolas added a commit that referenced this pull request Mar 11, 2026
- precompact-session-digest.cjs: spawn detached child process instead of
  require()-ing runner in-process. The runner uses setImmediate internally
  which kept the event loop alive and caused the hook to block (wait for
  the 9 s safety timeout) instead of being fire-and-forget. Context is
  forwarded via AIOX_HOOK_CONTEXT env var to the inline Node script.

- synapse-engine.cjs: replace process.exitCode = 0 with process.exit(0)
  in the timeout callback. process.exitCode alone does not terminate the
  process when active handles remain (e.g. stdout backpressure), defeating
  the 5 s hard limit that protects Claude Code from a blocking hook.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In @.claude/hooks/precompact-session-digest.cjs:
- Around line 93-104: The spawn call can emit an asynchronous 'error' event (and
JSON.stringify(context) can throw), so wrap the JSON.stringify(context) and
spawn(...) in a try/catch and, after obtaining the child from spawn, attach an
'error' listener on the returned ChildProcess (child.on('error', err => { /* log
or ignore */ })) to prevent unhandled exceptions; reference the inlineScript,
spawn(...), AIOX_HOOK_CONTEXT and onPreCompact usage and ensure any
serialization failure falls back to a safe/empty context before spawning.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 176707ae-4e10-4b5e-b1b1-4dcab8aa1769

📥 Commits

Reviewing files that changed from the base of the PR and between 1b56576 and ee758e1.

📒 Files selected for processing (2)
  • .claude/hooks/precompact-session-digest.cjs
  • .claude/hooks/synapse-engine.cjs

coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 11, 2026
@Pedrovaleriolopez
Copy link
Contributor

Review ⚡ @devops

Code: APPROVED ✅ — Both fixes are solid:

  1. ConfigCache.has() — correctly avoids stat inflation by checking TTL directly
  2. hook-runtime.js — properly creates session on first prompt to prevent silent updateSession() failures

Blocker: ❌ Merge conflict — please rebase against main:

git fetch origin && git rebase origin/main

Once rebased, this is ready to merge.

…497, #528)

Reimplemented has() to check Map directly without touching metrics.
Applied to both core/config/ and infrastructure/scripts/ copies.

runner path didn't account for node_modules layout. Added path resolution
with fallback candidates. Also fixed:
- synapse-engine.cjs: replaced process.exit() with process.exitCode for
  clean stdout flush
- hook-runtime.js: create session file on first prompt when loadSession
  returns null (prevents silent updateSession failures)

Updated tests to match new behavior (process.exitCode, createSession export).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
oalanicolas and others added 3 commits March 11, 2026 11:53
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- precompact-session-digest.cjs: spawn detached child process instead of
  require()-ing runner in-process. The runner uses setImmediate internally
  which kept the event loop alive and caused the hook to block (wait for
  the 9 s safety timeout) instead of being fire-and-forget. Context is
  forwarded via AIOX_HOOK_CONTEXT env var to the inline Node script.

- synapse-engine.cjs: replace process.exitCode = 0 with process.exit(0)
  in the timeout callback. process.exitCode alone does not terminate the
  process when active handles remain (e.g. stdout backpressure), defeating
  the 5 s hard limit that protects Claude Code from a blocking hook.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…t hook

Addresses CodeRabbit review: handle spawn 'error' event and
JSON.stringify failures to prevent unhandled exceptions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Pedrovaleriolopez Pedrovaleriolopez merged commit f74e3e7 into main Mar 11, 2026
36 checks passed
@Pedrovaleriolopez Pedrovaleriolopez deleted the fix/bugfix-497-528-config-cache-hooks branch March 11, 2026 20:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: agents Agent system related area: cli CLI tools (bin/, packages/aios-pro-cli/) area: core Core framework (.aios-core/core/) area: devops CI/CD, GitHub Actions (.github/) area: docs Documentation (docs/) area: health-check Health check system area: installer Installer and setup (packages/installer/) area: pro Pro features (pro/) area: synapse SYNAPSE context engine area: workflows Workflow system related mcp squad type: test Test coverage and quality

Projects

None yet

3 participants