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.
- package/.claude/hooks/search_enforcer.py +29 -2
- package/README.md +12 -2
- package/dist/cli/claude-recall-cli.js +7 -2
- package/dist/cli/commands/kiro-commands.js +1 -1
- package/dist/core/retrieval.js +1 -0
- package/dist/hooks/kiro-hooks.js +2 -1
- package/dist/hooks/llm-classifier.js +7 -2
- package/dist/hooks/post-compact-reload.js +4 -1
- package/dist/hooks/rule-injector.js +1 -0
- package/dist/hooks/subagent-hooks.js +4 -1
- package/dist/mcp/tools/memory-tools.js +15 -7
- package/dist/memory/schema.sql +0 -0
- package/dist/memory/storage.js +1 -1
- package/dist/pi/extension.js +3 -0
- package/dist/services/memory.js +10 -3
- package/dist/shared/event-processors.js +1 -1
- package/docs/hooks.md +1 -1
- package/package.json +1 -1
- package/scripts/uninstall.js +0 -0
- package/dist/cli/commands/live-test.js +0 -245
- package/dist/cli/commands/migrate.js +0 -317
- package/dist/core/patterns.js +0 -56
- package/dist/hooks/bash-failure-watcher.js +0 -253
- package/dist/mcp/memory-capture-middleware.js +0 -349
- package/dist/mcp/queue-tools.js +0 -532
- package/dist/mcp/tools/live-testing-tools.js +0 -231
- package/dist/mcp/tools/test-tools.js +0 -320
- package/dist/memory/database-adapter.js +0 -256
- package/dist/memory/pattern-store.js +0 -68
- package/dist/services/claude-json-watcher.js +0 -243
- package/dist/services/context-enhancer.js +0 -215
- package/dist/services/conversation-context-manager.js +0 -254
- package/dist/services/embedding-service.js +0 -183
- package/dist/services/memory-enhancer.js +0 -148
- package/dist/services/memory-evolution.js +0 -249
- package/dist/services/memory-usage-tracker.js +0 -227
- package/dist/services/preference-analyzer.js +0 -242
- package/dist/services/queue-api.js +0 -560
- package/dist/services/queue-integration.js +0 -409
- package/dist/services/queue-migration.js +0 -415
- package/dist/services/queue-system.js +0 -1092
- package/dist/services/restart-continuity.js +0 -361
- package/dist/services/semantic-preference-extractor.js +0 -432
- package/dist/testing/auto-correction-engine.js +0 -338
- package/dist/testing/live-testing-manager.js +0 -402
- package/dist/testing/mock-claude.js +0 -249
- package/dist/testing/observable-database.js +0 -178
- package/dist/testing/scenario-runner.js +0 -354
- package/dist/testing/test-orchestrator.js +0 -328
- package/docs/2026-07-follow-up-article-draft.md +0 -68
- package/docs/2026-07-follow-up-article-linkedin-tight.md +0 -46
- package/docs/2026-07-positioning-vs-steering-files.md +0 -92
- 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
|
-
|
|
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:
|
|
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` | `
|
|
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 = '
|
|
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) {
|
package/dist/core/retrieval.js
CHANGED
|
@@ -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,
|
package/dist/hooks/kiro-hooks.js
CHANGED
|
@@ -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);
|
|
@@ -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
|
},
|
package/dist/memory/schema.sql
CHANGED
|
File without changes
|
package/dist/memory/storage.js
CHANGED
|
@@ -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).
|
package/dist/pi/extension.js
CHANGED
|
@@ -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
|
}
|
package/dist/services/memory.js
CHANGED
|
@@ -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` | `
|
|
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
package/scripts/uninstall.js
CHANGED
|
File without changes
|