claude-recall 0.36.2 → 0.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.claude/hooks/search_enforcer.py +29 -2
  2. package/README.md +12 -2
  3. package/dist/cli/claude-recall-cli.js +7 -2
  4. package/dist/cli/commands/kiro-commands.js +1 -1
  5. package/dist/core/retrieval.js +1 -0
  6. package/dist/hooks/kiro-hooks.js +2 -1
  7. package/dist/hooks/llm-classifier.js +7 -2
  8. package/dist/hooks/post-compact-reload.js +4 -1
  9. package/dist/hooks/rule-injector.js +1 -0
  10. package/dist/hooks/subagent-hooks.js +4 -1
  11. package/dist/mcp/tools/memory-tools.js +15 -7
  12. package/dist/memory/schema.sql +0 -0
  13. package/dist/memory/storage.js +1 -1
  14. package/dist/pi/extension.js +3 -0
  15. package/dist/services/memory.js +10 -3
  16. package/dist/shared/event-processors.js +1 -1
  17. package/docs/hooks.md +1 -1
  18. package/package.json +1 -1
  19. package/scripts/uninstall.js +0 -0
  20. package/dist/cli/commands/live-test.js +0 -245
  21. package/dist/cli/commands/migrate.js +0 -317
  22. package/dist/core/patterns.js +0 -56
  23. package/dist/hooks/bash-failure-watcher.js +0 -253
  24. package/dist/mcp/memory-capture-middleware.js +0 -349
  25. package/dist/mcp/queue-tools.js +0 -532
  26. package/dist/mcp/tools/live-testing-tools.js +0 -231
  27. package/dist/mcp/tools/test-tools.js +0 -320
  28. package/dist/memory/database-adapter.js +0 -256
  29. package/dist/memory/pattern-store.js +0 -68
  30. package/dist/services/claude-json-watcher.js +0 -243
  31. package/dist/services/context-enhancer.js +0 -215
  32. package/dist/services/conversation-context-manager.js +0 -254
  33. package/dist/services/embedding-service.js +0 -183
  34. package/dist/services/memory-enhancer.js +0 -148
  35. package/dist/services/memory-evolution.js +0 -249
  36. package/dist/services/memory-usage-tracker.js +0 -227
  37. package/dist/services/preference-analyzer.js +0 -242
  38. package/dist/services/queue-api.js +0 -560
  39. package/dist/services/queue-integration.js +0 -409
  40. package/dist/services/queue-migration.js +0 -415
  41. package/dist/services/queue-system.js +0 -1092
  42. package/dist/services/restart-continuity.js +0 -361
  43. package/dist/services/semantic-preference-extractor.js +0 -432
  44. package/dist/testing/auto-correction-engine.js +0 -338
  45. package/dist/testing/live-testing-manager.js +0 -402
  46. package/dist/testing/mock-claude.js +0 -249
  47. package/dist/testing/observable-database.js +0 -178
  48. package/dist/testing/scenario-runner.js +0 -354
  49. package/dist/testing/test-orchestrator.js +0 -328
  50. package/docs/2026-07-follow-up-article-draft.md +0 -68
  51. package/docs/2026-07-follow-up-article-linkedin-tight.md +0 -46
  52. package/docs/2026-07-positioning-vs-steering-files.md +0 -92
  53. package/docs/using-claude-code-subscription-instead-of-api-key.md +0 -69
@@ -16,7 +16,29 @@ from datetime import datetime
16
16
 
17
17
  STATE_DIR = Path.home() / '.claude-recall' / 'hook-state'
18
18
  SEARCH_TTL_MS = int(os.environ.get('CLAUDE_RECALL_SEARCH_TTL', 60 * 1000)) # 1 min default (once per task)
19
- ENFORCE_MODE = os.environ.get('CLAUDE_RECALL_ENFORCE_MODE', 'block') # block, warn, off
19
+
20
+
21
+ def _resolve_enforce_mode() -> str:
22
+ """Enforcement mode: env var wins, then ~/.claude-recall/config.json
23
+ ("enforceMode"), else default 'warn'. A file-based switch is reachable
24
+ from inside a running session (an agent can edit config.json to escape a
25
+ stuck gate); an env var set before launch is not. Default is 'warn' — this
26
+ gate is an advisory nudge (see module docstring), so it should never
27
+ hard-block a session by default."""
28
+ env = os.environ.get('CLAUDE_RECALL_ENFORCE_MODE')
29
+ if env:
30
+ return env.strip().lower()
31
+ try:
32
+ cfg = json.load(open(Path.home() / '.claude-recall' / 'config.json'))
33
+ mode = cfg.get('enforceMode')
34
+ if mode:
35
+ return str(mode).strip().lower()
36
+ except Exception:
37
+ pass
38
+ return 'warn'
39
+
40
+
41
+ ENFORCE_MODE = _resolve_enforce_mode() # block, warn, off
20
42
  MAX_BLOCKS = int(os.environ.get('CLAUDE_RECALL_MAX_BLOCKS', 3)) # degrade to warn after N blocks
21
43
 
22
44
  # Tools that count as "search performed"
@@ -182,6 +204,7 @@ STALE RULES — consider reloading before {tool_name}
182
204
 
183
205
  Rules were loaded earlier but TTL expired.
184
206
  Run: mcp__claude-recall__load_rules({{}})
207
+ (If not directly callable, ToolSearch it first — see below.)
185
208
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
186
209
  """
187
210
  print(msg.strip(), file=sys.stderr)
@@ -214,10 +237,14 @@ LOAD RULES REQUIRED before {tool_name} (attempt {block_count}/{MAX_BLOCKS})
214
237
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
215
238
 
216
239
  Run: mcp__claude-recall__load_rules({{}})
240
+ (If that tool is not directly callable, first run
241
+ ToolSearch({{query:"select:mcp__claude-recall__load_rules"}}) —
242
+ some harnesses defer MCP tool schemas until discovered.)
217
243
 
218
244
  This ensures you apply user preferences and avoid past mistakes.
219
245
 
220
- To disable: CLAUDE_RECALL_ENFORCE_MODE=off
246
+ To disable this gate: set "enforceMode":"off" in ~/.claude-recall/config.json
247
+ (or export CLAUDE_RECALL_ENFORCE_MODE=off before launching Claude Code).
221
248
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
222
249
  """
223
250
  print(msg.strip(), file=sys.stderr)
package/README.md CHANGED
@@ -191,6 +191,7 @@ Once installed, Claude Recall works in the background (CC = Claude Code):
191
191
  | **Sub-agent spawned** | Rules are injected into the sub-agent; its outcome is captured | ✓ | | |
192
192
  | **Session exit** | An auto-checkpoint (`{completed, remaining, blockers}`) is saved for next time | ✓ | ✓ | |
193
193
  | **End of session** | Failure patterns become candidate lessons; validated ones are promoted to rules | ✓ | ✓ | |
194
+ | **Hard-won success** | A goal that failed repeatedly then finally worked is captured as a reusable `solution` ([details](#success-capture)) | ✓ | ✓ | |
194
195
  | **Once a day** | The memory janitor reviews stored rules with the runtime's LLM: demotes noise, merges duplicates, rewrites vague rules ([details](#memory-janitor)) | ✓ | | ✓ |
195
196
 
196
197
  Classification runs on each runtime's **own** LLM — Claude Code via headless `claude -p` on your subscription; Kiro via `kiro-cli chat --no-interactive` on Kiro credits — with regex as the fallback. **An exported `ANTHROPIC_API_KEY` is never touched** unless you explicitly opt in with `CLAUDE_RECALL_PREFER_API_KEY=1`. No API key is ever required; no configuration needed.
@@ -277,7 +278,7 @@ claude-recall stats # Memory statistics (--global for all p
277
278
  claude-recall list # List memories, newest first (-t <type>, --all, --json, --global)
278
279
  claude-recall search "query" # Search memories (--global, --json, --project <id>)
279
280
  claude-recall store "content" # Store memory directly
280
- claude-recall store "content" -t <type> # Type: preference, correction, failure, devops, project-knowledge
281
+ claude-recall store "content" -t <type> # Type: preference, correction, failure, devops, project-knowledge, solution
281
282
  claude-recall export backup.json # Export current project (--global for all)
282
283
  claude-recall import backup.json # Import memories from JSON
283
284
  claude-recall delete <key> # Delete one memory by key (get keys from `search`)
@@ -353,6 +354,15 @@ action → outcome event → episode → candidate lesson → promotion → acti
353
354
 
354
355
  Failures become candidate lessons (deduplicated by similarity); lessons seen 2+ times (or once, if severe) are promoted to active rules; every just-in-time injection (Claude Code, Pi) is recorded and resolved against the tool's outcome, building per-rule effectiveness data over time.
355
356
 
357
+ ### Success capture
358
+
359
+ Auto-capture is failure-biased by design — it learns from what breaks. But a hard-won *success* is just as reusable: the command, flag, or sequence you finally landed after several dead ends. Claude Recall captures those as a first-class `solution` memory, two ways:
360
+
361
+ - **Automatically**, when a session shows a goal that failed **repeatedly** (≥2 distinct failed attempts) and then finally worked — it stores the reusable technique, generalized away from the one-off task. The multi-failure gate is deliberate: a first-try success or an unresolved struggle captures nothing, so routine wins don't become noise.
362
+ - **Deliberately**, when you (or the agent) call `store_memory` with `type: "solution"` — the intended home for "I cracked this, don't make me re-derive it."
363
+
364
+ A solution is active immediately (no wait for a second occurrence — you rarely crack the same hard thing twice), injected at every surface alongside your other rules, and ranked just below explicit corrections. List them with `claude-recall list --type solution`. Unlike other rules, solutions are exempt from the never-cited auto-demote sweep, so a rarely-needed-but-valuable win isn't retired.
365
+
356
366
  ### Memory janitor
357
367
 
358
368
  Automatic capture inevitably stores some noise — a conversational fragment misfiled as a preference, the same lesson in five wordings, a rule too vague to act on. Counters can flag *unused* rules (`CLAUDE_RECALL_AUTO_DEMOTE`), but they can't tell a rarely-cited gem from junk. Once a day, the **memory janitor** has the runtime's own LLM (same backend policy as capture — your Claude subscription or Kiro credits, never an API key unless you opted in) review the stored rules and:
@@ -450,7 +460,7 @@ Defaults work out of the box; tune via environment variables as needed.
450
460
  | `CLAUDE_RECALL_AUTO_CLEANUP` | `false` | Auto-kill stale MCP processes on start (otherwise reports and exits). |
451
461
  | `CLAUDE_RECALL_COMPACT_THRESHOLD` | `10MB` | DB size at which automatic compaction kicks in. |
452
462
  | `CLAUDE_RECALL_MAX_MEMORIES` | `10000` | Memory-row soft cap. |
453
- | `CLAUDE_RECALL_ENFORCE_MODE` | `on` | Set to `off` to bypass the search-enforcer hook. |
463
+ | `CLAUDE_RECALL_ENFORCE_MODE` | `warn` | `block` / `warn` / `off` for the search-enforcer hook. Env wins, else `~/.claude-recall/config.json` `"enforceMode"`, else `warn`. |
454
464
  | `CLAUDE_RECALL_LLM_TIMEOUT_MS` | `5000` | Timeout for hook-context LLM calls (classification, hindsight hints). Hooks fall back to regex when it fires. |
455
465
  | `CLAUDE_RECALL_STOP_DEBOUNCE_MS` | `300000` | Debounce for the heavy Stop-hook pipeline (episodes, session extraction, promotion). `0` disables. |
456
466
  | `CLAUDE_RECALL_PROJECT_ID` | *(cwd)* | Pin the project scope to a fixed id, overriding working-directory detection. |
@@ -51,9 +51,10 @@ const hook_commands_1 = require("./commands/hook-commands");
51
51
  const kiro_commands_1 = require("./commands/kiro-commands");
52
52
  const repair_1 = require("./commands/repair");
53
53
  // v14 = add PreToolUse rule-injector + Post resolver for JITRI.
54
+ // v15 = bound the search_enforcer PreToolUse entry with timeout: 5.
54
55
  // Bump when the hook block template changes — setup skips the settings
55
56
  // rewrite when the installed hooksVersion already matches.
56
- const HOOKS_VERSION = '14.0.0';
57
+ const HOOKS_VERSION = '15.0.0';
57
58
  const parse_utils_1 = require("./parse-utils");
58
59
  const program = new commander_1.Command();
59
60
  class ClaudeRecallCLI {
@@ -1375,7 +1376,11 @@ async function main() {
1375
1376
  hooks: [
1376
1377
  {
1377
1378
  type: "command",
1378
- command: `python3 ${hookDest}`
1379
+ command: `python3 ${hookDest}`,
1380
+ // Runs before EVERY tool call (matcher .*). Bound the python3
1381
+ // cold start so it can't become a per-call latency tax or an
1382
+ // unbounded failure mode.
1383
+ timeout: 5
1379
1384
  },
1380
1385
  {
1381
1386
  type: "command",
@@ -450,7 +450,7 @@ class KiroCommands {
450
450
  : `project: ${projectId} (from working directory)`);
451
451
  line('•', `total memories (all projects): ${stats.total}`);
452
452
  const rules = ms.loadActiveRules(projectId);
453
- const ruleCount = rules.preferences.length + rules.corrections.length + rules.failures.length + rules.devops.length;
453
+ const ruleCount = rules.preferences.length + rules.corrections.length + rules.failures.length + rules.devops.length + (rules.solutions ?? []).length;
454
454
  line(ruleCount > 0 ? '✓' : '•', `active rules for this project: ${ruleCount}`);
455
455
  }
456
456
  catch (err) {
@@ -270,6 +270,7 @@ class MemoryRetrieval {
270
270
  exports.MemoryRetrieval = MemoryRetrieval;
271
271
  MemoryRetrieval.TYPE_PRIORITY = {
272
272
  'correction': 6,
273
+ 'solution': 5.5, // hard-won reusable solutions — high signal, rank just below corrections
273
274
  'project-knowledge': 5,
274
275
  'preference': 4,
275
276
  'devops': 3,
@@ -125,11 +125,12 @@ function formatRulesForContext() {
125
125
  const sections = [
126
126
  section('Preferences', rules.preferences),
127
127
  section('Corrections', rules.corrections),
128
+ section('Solutions (hard-won — reuse these)', rules.solutions ?? []),
128
129
  section('Failures', rules.failures),
129
130
  section('DevOps Rules', rules.devops),
130
131
  ].filter((s) => s !== null);
131
132
  const total = rules.preferences.length + rules.corrections.length
132
- + rules.failures.length + rules.devops.length;
133
+ + rules.failures.length + rules.devops.length + (rules.solutions ?? []).length;
133
134
  return { body: sections.join('\n\n'), total };
134
135
  }
135
136
  /**
@@ -257,6 +257,11 @@ const SESSION_EXTRACTION_PROMPT = `You are analyzing a coding session transcript
257
257
 
258
258
  The transcript shows tool calls (Bash, Edit, Read, Grep, etc.) and their results, plus user and assistant messages. Your primary job is to identify CAUSE-AND-EFFECT patterns — what failed, why, and what fixed it.
259
259
 
260
+ PRIORITY 0 — Hard-won solutions (type "solution"):
261
+ The MOST valuable thing to capture. Look for a goal the agent attempted, FAILED at REPEATEDLY (two or more distinct failed attempts, errors, wrong approaches, or retries on the same objective), and then FINALLY got working. Capture the reusable technique that cracked it — the command, config, flag, sequence, or approach that worked — phrased so a FUTURE session facing the same goal can apply it directly.
262
+ STRICT gate: only emit a "solution" when there is clear evidence of (a) multiple failed attempts AND (b) an eventual success on that same goal. A task that worked first try, or that never resolved, is NOT a solution — skip it.
263
+ Generalize away the one-off specifics (this repo's file names, this task's data) but KEEP the reusable mechanism. Example: after several failed attempts, "To submit to a Kaggle competition programmatically, use \`kaggle competitions submit -c <slug> -f <file> -m <msg>\` after \`kaggle config set -n competition -v <slug>\` — the web-form flow can't be scripted."
264
+
260
265
  PRIORITY 1 — Failure → Fix sequences:
261
266
  Look for tool calls that failed (errors, timeouts, non-zero exits) followed by a different approach that succeeded. Extract the lesson as an imperative rule.
262
267
  Examples:
@@ -280,7 +285,7 @@ Do NOT extract:
280
285
  - Anything in the EXISTING MEMORIES list below
281
286
 
282
287
  Respond with ONLY valid JSON (no markdown fences):
283
- [{"type":"project-knowledge|preference|devops|failure","content":"<imperative statement>","confidence":0.0-1.0}]
288
+ [{"type":"solution|project-knowledge|preference|devops|failure","content":"<imperative statement>","confidence":0.0-1.0}]
284
289
 
285
290
  Return [] if nothing durable was learned. Max 10 items. Each content should be a concise, actionable rule (e.g. "Pipe 'y' to scripts/upgrade-sandbox.sh — it has an interactive confirmation prompt").`;
286
291
  /**
@@ -299,7 +304,7 @@ async function extractSessionLearningsWithLLM(summary, existingMemories) {
299
304
  const results = parseJSON(text);
300
305
  if (!Array.isArray(results))
301
306
  return null;
302
- const validTypes = ['project-knowledge', 'preference', 'devops', 'failure'];
307
+ const validTypes = ['project-knowledge', 'preference', 'devops', 'failure', 'solution'];
303
308
  return results
304
309
  .filter((r) => r && validTypes.includes(r.type) && typeof r.content === 'string' && r.content.length > 5)
305
310
  .map((r) => ({
@@ -31,6 +31,9 @@ function formatRules(rules) {
31
31
  if (rules.corrections.length > 0) {
32
32
  sections.push('## Corrections\n' + rules.corrections.map(m => `- ${extractVal(m.value)}`).join('\n'));
33
33
  }
34
+ if ((rules.solutions ?? []).length > 0) {
35
+ sections.push('## Solutions (hard-won — reuse these)\n' + rules.solutions.map(m => `- ${extractVal(m.value)}`).join('\n'));
36
+ }
34
37
  if (rules.failures.length > 0) {
35
38
  sections.push('## Failures\n' + rules.failures.map(m => `- ${extractVal(m.value)}`).join('\n'));
36
39
  }
@@ -44,7 +47,7 @@ async function handlePostCompactReload(_input) {
44
47
  const projectId = config_1.ConfigService.getInstance().getProjectId();
45
48
  const rules = memory_1.MemoryService.getInstance().loadActiveRules(projectId);
46
49
  const totalRules = rules.preferences.length + rules.corrections.length +
47
- rules.failures.length + rules.devops.length;
50
+ rules.failures.length + rules.devops.length + (rules.solutions ?? []).length;
48
51
  if (totalRules === 0)
49
52
  return;
50
53
  const body = formatRules(rules);
@@ -108,6 +108,7 @@ async function computeInjection(toolName, toolInput, toolUseId) {
108
108
  ...activeRules.corrections,
109
109
  ...activeRules.failures,
110
110
  ...activeRules.devops,
111
+ ...(activeRules.solutions ?? []),
111
112
  ].map(m => ({
112
113
  key: m.key,
113
114
  type: m.type,
@@ -34,6 +34,9 @@ function formatRulesCompact(rules) {
34
34
  if (rules.corrections.length > 0) {
35
35
  sections.push('Corrections:\n' + rules.corrections.map(m => `- ${extractVal(m.value)}`).join('\n'));
36
36
  }
37
+ if ((rules.solutions ?? []).length > 0) {
38
+ sections.push('Solutions (hard-won — reuse these):\n' + rules.solutions.map(m => `- ${extractVal(m.value)}`).join('\n'));
39
+ }
37
40
  if (rules.failures.length > 0) {
38
41
  sections.push('Failures:\n' + rules.failures.map(m => `- ${extractVal(m.value)}`).join('\n'));
39
42
  }
@@ -52,7 +55,7 @@ async function handleSubagentStart(input) {
52
55
  const projectId = config_1.ConfigService.getInstance().getProjectId();
53
56
  const rules = memory_1.MemoryService.getInstance().loadActiveRules(projectId);
54
57
  const totalRules = rules.preferences.length + rules.corrections.length +
55
- rules.failures.length + rules.devops.length;
58
+ rules.failures.length + rules.devops.length + (rules.solutions ?? []).length;
56
59
  if (totalRules === 0)
57
60
  return;
58
61
  const body = formatRulesCompact(rules);
@@ -108,7 +108,7 @@ class MemoryTools {
108
108
  },
109
109
  {
110
110
  name: 'store_memory',
111
- description: 'Store a rule or learning. Use for: corrections, preferences, devops rules, failures. The stored rule is immediately active in this conversation.',
111
+ description: 'Store a rule or learning. Use for: corrections, preferences, devops rules, failures, and solutions. IMPORTANT: when you crack a hard problem after real trial-and-error (a working command, config, or sequence you had to discover), store it with type "solution" — auto-capture only learns from failures, never from your wins, so a hard-won success is lost unless you save it here. The stored rule is immediately active in this conversation.',
112
112
  inputSchema: {
113
113
  type: 'object',
114
114
  properties: {
@@ -118,7 +118,7 @@ class MemoryTools {
118
118
  },
119
119
  metadata: {
120
120
  type: 'object',
121
- description: 'Optional metadata. Set "type" to one of: preference, correction, devops, failure'
121
+ description: 'Optional metadata. Set "type" to one of: preference, correction, devops, failure, solution (use "solution" for a hard-won working fix you discovered through trial-and-error)'
122
122
  },
123
123
  scope: {
124
124
  type: 'string',
@@ -146,7 +146,7 @@ class MemoryTools {
146
146
  },
147
147
  type: {
148
148
  type: 'string',
149
- description: 'Filter by memory type: preference, correction, devops, failure, project-knowledge'
149
+ description: 'Filter by memory type: preference, correction, devops, failure, project-knowledge, solution'
150
150
  },
151
151
  projectId: {
152
152
  type: 'string',
@@ -221,7 +221,7 @@ class MemoryTools {
221
221
  throw new Error('Content is required and must be a string');
222
222
  }
223
223
  // Use metadata.type as the memory type so it appears in future load_rules calls
224
- const validTypes = ['preference', 'correction', 'devops', 'failure', 'project-knowledge', 'tool-use'];
224
+ const validTypes = ['preference', 'correction', 'devops', 'failure', 'project-knowledge', 'solution', 'tool-use'];
225
225
  const detectedType = (metadata?.type && validTypes.includes(metadata.type))
226
226
  ? metadata.type
227
227
  : 'preference';
@@ -336,8 +336,9 @@ class MemoryTools {
336
336
  }
337
337
  return kept;
338
338
  };
339
- // Allocate in priority order (corrections first to protect high-signal items).
339
+ // Allocate in priority order (corrections + solutions first to protect high-signal items).
340
340
  const keptCorrections = takeBounded(rules.corrections);
341
+ const keptSolutions = takeBounded([...(rules.solutions ?? [])].sort(byCiteThenFresh));
341
342
  const keptPreferences = takeBounded([...rules.preferences].sort(byCiteThenFresh));
342
343
  const keptDevops = takeBounded([...rules.devops].sort(byCiteThenFresh));
343
344
  const keptFailures = takeBounded(rules.failures, 3);
@@ -376,6 +377,12 @@ class MemoryTools {
376
377
  }).join('\n'));
377
378
  }
378
379
  }
380
+ if (keptSolutions.length > 0) {
381
+ sections.push('## Solutions (hard-won — reuse these before re-deriving)\n' + keptSolutions.map(m => {
382
+ const val = formatRuleValue(m.value) + precisionNudge(m.value);
383
+ return `- ${val}`;
384
+ }).join('\n'));
385
+ }
379
386
  if (keptDevops.length > 0) {
380
387
  sections.push('## DevOps Rules\n' + keptDevops.map(m => {
381
388
  const val = formatRuleValue(m.value) + precisionNudge(m.value);
@@ -388,8 +395,8 @@ class MemoryTools {
388
395
  sections.push(`*${droppedCount} more rules available via \`search_memory\`. Run \`npx claude-recall outcomes\` for full stats.*`);
389
396
  }
390
397
  const totalRules = keptPreferences.length + keptCorrections.length +
391
- keptFailures.length + keptDevops.length;
392
- const keptAll = [...keptPreferences, ...keptCorrections, ...keptFailures, ...keptDevops];
398
+ keptFailures.length + keptDevops.length + keptSolutions.length;
399
+ const keptAll = [...keptPreferences, ...keptCorrections, ...keptFailures, ...keptDevops, ...keptSolutions];
393
400
  const resultTokens = this.estimateTokens(keptAll);
394
401
  // Record to SearchMonitor so monitoring/stats still work
395
402
  this.searchMonitor.recordSearch('load_rules', totalRules, context.sessionId, 'mcp', { tool: 'load_rules', tokenMetrics: { resultTokens, tokensSaved: totalRules > 0 ? totalRules * 200 : 0 } });
@@ -436,6 +443,7 @@ class MemoryTools {
436
443
  corrections: keptCorrections.length,
437
444
  failures: keptFailures.length,
438
445
  devops: keptDevops.length,
446
+ solutions: keptSolutions.length,
439
447
  total: totalRules,
440
448
  dropped: droppedCount,
441
449
  },
File without changes
@@ -1060,7 +1060,7 @@ class MemoryStorage {
1060
1060
  }
1061
1061
  exports.MemoryStorage = MemoryStorage;
1062
1062
  /** Rule-type memories: the only types subject to fuzzy dedup and retro-dedup. */
1063
- MemoryStorage.RULE_TYPES = ['preference', 'correction', 'failure', 'devops', 'project-knowledge'];
1063
+ MemoryStorage.RULE_TYPES = ['preference', 'correction', 'failure', 'devops', 'project-knowledge', 'solution'];
1064
1064
  /**
1065
1065
  * Supersession sentinels written by automatic hygiene passes (as opposed to
1066
1066
  * a USER preference override, where superseded_by names the winning key).
@@ -71,6 +71,9 @@ function formatRules(rules) {
71
71
  if (rules.corrections.length > 0) {
72
72
  sections.push('## Corrections\n' + rules.corrections.map(m => `- ${extractVal(m.value)}`).join('\n'));
73
73
  }
74
+ if ((rules.solutions ?? []).length > 0) {
75
+ sections.push('## Solutions (hard-won — reuse these)\n' + rules.solutions.map(m => `- ${extractVal(m.value)}`).join('\n'));
76
+ }
74
77
  if (rules.failures.length > 0) {
75
78
  sections.push('## Failures\n' + rules.failures.map(m => `- ${extractVal(m.value)}`).join('\n'));
76
79
  }
@@ -456,11 +456,18 @@ class MemoryService {
456
456
  .slice(0, 5);
457
457
  // DevOps: all active rules
458
458
  const devops = this.storage.searchByContext({ ...searchContext, type: 'devops' }).filter(isActive);
459
+ // Solutions: hard-won reusable resolutions, top 5 by timestamp
460
+ const allSolutions = this.storage.searchByContext({ ...searchContext, type: 'solution' });
461
+ const solutions = allSolutions
462
+ .filter(isActive)
463
+ .sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))
464
+ .slice(0, 5);
459
465
  const counts = [
460
466
  preferences.length && `${preferences.length} preferences`,
461
467
  corrections.length && `${corrections.length} corrections`,
462
468
  failures.length && `${failures.length} failures`,
463
469
  devops.length && `${devops.length} devops rules`,
470
+ solutions.length && `${solutions.length} solutions`,
464
471
  ].filter(Boolean);
465
472
  const summary = counts.length > 0
466
473
  ? `Loaded ${counts.join(', ')}`
@@ -468,16 +475,16 @@ class MemoryService {
468
475
  this.logger.info('MemoryService', summary, { projectId: pid });
469
476
  // Increment load_count for all returned rules
470
477
  const allIds = [
471
- ...preferences, ...corrections, ...failures, ...devops
478
+ ...preferences, ...corrections, ...failures, ...devops, ...solutions
472
479
  ].map(m => m.id).filter((id) => id !== undefined);
473
480
  if (allIds.length > 0) {
474
481
  this.storage.incrementLoadCounts(allIds);
475
482
  }
476
- return { preferences, corrections, failures, devops, summary };
483
+ return { preferences, corrections, failures, devops, solutions, summary };
477
484
  }
478
485
  catch (error) {
479
486
  this.logger.logServiceError('MemoryService', 'loadActiveRules', error);
480
- return { preferences: [], corrections: [], failures: [], devops: [], summary: 'Error loading rules' };
487
+ return { preferences: [], corrections: [], failures: [], devops: [], solutions: [], summary: 'Error loading rules' };
481
488
  }
482
489
  }
483
490
  /**
@@ -440,7 +440,7 @@ async function extractSessionLearnings(entries, sessionId, projectId, maxStore =
440
440
  try {
441
441
  const ms = memory_1.MemoryService.getInstance();
442
442
  const rules = ms.loadActiveRules(projectId);
443
- const all = [...rules.preferences, ...rules.corrections, ...rules.failures, ...rules.devops];
443
+ const all = [...rules.preferences, ...rules.corrections, ...rules.failures, ...rules.devops, ...(rules.solutions ?? [])];
444
444
  for (const m of all.slice(0, 20)) {
445
445
  const val = typeof m.value === 'object' ? (m.value?.content || JSON.stringify(m.value)) : String(m.value);
446
446
  existingMemories.push(truncate(val, 80));
package/docs/hooks.md CHANGED
@@ -58,7 +58,7 @@ Common read-only commands are exempt from enforcement:
58
58
  | Environment Variable | Default | Description |
59
59
  |---|---|---|
60
60
  | `CLAUDE_RECALL_SEARCH_TTL` | `300000` (5 min) | Milliseconds a search remains valid |
61
- | `CLAUDE_RECALL_ENFORCE_MODE` | `block` | `block` (exit 2), `warn` (exit 0 + stderr message), or `off` (disabled) |
61
+ | `CLAUDE_RECALL_ENFORCE_MODE` | `warn` | `block` (exit 2), `warn` (exit 0 + stderr message), or `off` (disabled). Env wins; otherwise read from `~/.claude-recall/config.json` → `"enforceMode"`; else `warn`. The file switch is reachable from inside a running session — an env var set before launch is not. |
62
62
 
63
63
  ### Exit Codes
64
64
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-recall",
3
- "version": "0.36.2",
3
+ "version": "0.37.1",
4
4
  "description": "Persistent memory for Claude Code and Pi with native Skills integration, automatic capture, failure learning, and project scoping",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
File without changes