Skip to content

fix(deep-consolidation): isolate inner claude --print from caller's cwd#2

Open
eantones wants to merge 1 commit into
dp-web4:mainfrom
eantones:fix/deep-consolidation-cwd-isolation
Open

fix(deep-consolidation): isolate inner claude --print from caller's cwd#2
eantones wants to merge 1 commit into
dp-web4:mainfrom
eantones:fix/deep-consolidation-cwd-isolation

Conversation

@eantones
Copy link
Copy Markdown

@eantones eantones commented May 6, 2026

Problem

Every invocation of the deep dream cycle silently returns zero LLM-generated patterns. Logs show:

[snarc] Deep consolidation failed: spawnSync /bin/sh ETIMEDOUT
[snarc] Dream cycle: NNN created, MM decayed, KK pruned

The heuristic (regex-based) dream cycle keeps producing Tier 2 patterns, which masks the failure — observers see "patterns being created" and assume the system is healthy, but the LLM-extracted high-quality patterns (deep_*) are absent.

Reproduction

Trigger snarc dream --deep from any cwd that contains a project-specific CLAUDE.md and/or a .claude/settings.json with SessionStart hooks. This is a common setup when SNARC is embedded as a memory backend behind a project workspace.

Root cause

src/deep-consolidation.ts invokes the LLM helper like this:

response = execSync(
  `cat "${tmpFile}" | claude --print -`,
  {
    timeout: 60_000,
    encoding: 'utf-8',
    stdio: ['pipe', 'pipe', 'pipe'],
  },
).trim();

No cwd option is passed, so execSync inherits the parent process's cwd. When SNARC runs as a memory-consolidation pass from inside a project workspace, the inner claude --print therefore starts in that workspace's directory — picking up its CLAUDE.md, its SessionStart hooks, its state-continuity pin, and any edit-validation hooks.

Two compounding effects:

  1. The inner Claude loads project context that biases it toward agent-style behavior (treating itself as a participant in the project) and returns prose narration instead of the JSON array the consolidation prompt requested. response.match(/\[[\s\S]*\]/) then finds no JSON array.
  2. State-continuity hooks try to write to a state pin that the inner Claude shouldn't be touching; the resulting validation block elicits a meta-explanation in the response. The added overhead can also push the inner call past the 60 s timeout, which is what surfaces as the user-visible ETIMEDOUT.

A representative response (paraphrased, observed after instrumentation):

"The state file edit was denied. Since this turn was a memory consolidation pass (not project work), the existing state still accurately reflects the project's posture. No new project decisions or actions to record. Ending turn as instructed."

The inner Claude even correctly identifies its role ("this turn was a memory consolidation pass, not project work") — but the project hooks override the prompt's authority.

Fix

Add cwd: tmpdir() to the execSync options:

     response = execSync(
       `cat "${tmpFile}" | claude --print -`,
       {
+        cwd: tmpdir(),
         timeout: 60_000,
         encoding: 'utf-8',
         stdio: ['pipe', 'pipe', 'pipe'],
       },
     ).trim();

The inner claude --print now runs from the OS temp directory, where no project-level CLAUDE.md or SessionStart hooks exist. It operates in a clean context, follows the prompt, and returns the JSON array. With hooks no longer loading, the call also completes well within the existing 60 s timeout.

A multi-line comment is added at the call site explaining the rationale, so future readers don't undo the cwd isolation thinking it's accidental.

Validation

Tested against a real-world database with 37,094 observations:

  • Pre-fix: every deep dream cycle returned 0 LLM patterns; ETIMEDOUT and "no JSON array in response" in logs.
  • Post-fix: 938 → 946 patterns (+8 high-quality deep_* entries — workflows, insights, decisions, error fixes), wall time 32.5 s, 0 errors.

The 8 patterns extracted in the validation run were manually inspected and all were genuinely useful (no noise, no over-fitting on transient observations).

Notes

  • The same pattern (forgetting to pass cwd to execSync of an LLM CLI) is easy to introduce elsewhere; src/deep-consolidation.ts is the only such site I see in the current tree, but worth keeping in mind for future helpers.
  • The bug has existed since 31255b5 ("feat: deep dream — LLM-powered consolidation via claude --print"). It only manifests when the cwd has agent-style configuration, which is why it has gone unnoticed in clean test environments.

The deep dream cycle invokes `claude --print` via execSync but never
sets cwd, so the inner Claude inherits whichever cwd SNARC was triggered
from (typically the parent agent's case folder). The inner Claude then
loads the parent's project CLAUDE.md + SessionStart hooks (state-continuity
pointing at a state pin, edit-validation hooks, etc.). With those in
scope, the inner Claude is steered toward agent-style behavior and
returns prose narration instead of the JSON array the prompt asks for.
The result is a silent failure on every agent close:

  [snarc] Deep consolidation: no JSON array in response

The fix is to set cwd: tmpdir() on the execSync call. With no project
CLAUDE.md or project hooks in scope, the inner Claude treats the prompt
as the only instruction and returns the JSON array as requested.

Verified end-to-end: 8 high-quality deep_workflow / deep_insight /
deep_decision / deep_error_fix patterns extracted from a 37k-observation
DB on first post-fix invocation, 32s wall time.
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