claude-recall 0.17.1 → 0.18.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.
@@ -8,7 +8,7 @@ source: claude-recall
8
8
 
9
9
  # Corrections
10
10
 
11
- Auto-generated from 21 memories. Last updated: 2026-03-17.
11
+ Auto-generated from 22 memories. Last updated: 2026-03-20.
12
12
 
13
13
  ## Rules
14
14
 
@@ -26,6 +26,7 @@ Auto-generated from 21 memories. Last updated: 2026-03-17.
26
26
  - CORRECTION: Memory with complex metadata
27
27
  - CORRECTION: Memory with complex metadata
28
28
  - CORRECTION: Memory with complex metadata
29
+ - CORRECTION: Memory with complex metadata
29
30
  - CORRECTION: Replace expired access token with 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
30
31
  - CORRECTION: use spaces not tabs for indentation
31
32
  - CORRECTION: cited (loaded 5+ times): 19
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "topicId": "corrections",
3
- "sourceHash": "b717cebe14486cb8c7d1681f494e3e31136c32b0fe8a51112fe1c1a07bfe47f4",
4
- "memoryCount": 21,
5
- "generatedAt": "2026-03-17T09:43:28.791Z",
3
+ "sourceHash": "71d234bd5cea462bcfc192c4b421fc4d28d920f188c8393f0b3621e86092febe",
4
+ "memoryCount": 22,
5
+ "generatedAt": "2026-03-20T09:33:07.652Z",
6
6
  "memoryKeys": [
7
+ "memory_1773999187635_v9sxfphej",
7
8
  "memory_1773740608781_ae2bn7zos",
8
9
  "memory_1773659723317_o738xfbyw",
9
10
  "memory_1773656489899_l75d50pxo",
@@ -8,11 +8,12 @@ source: claude-recall
8
8
 
9
9
  # Failure Lessons
10
10
 
11
- Auto-generated from 24 memories. Last updated: 2026-03-17.
11
+ Auto-generated from 25 memories. Last updated: 2026-03-20.
12
12
 
13
13
  ## Rules
14
14
 
15
15
  - SQLite query syntax error: LIKE clause requires single quotes around string literal, not double quotes
16
+ - Avoid: Command failed: npx jest tests/unit/bash-failure-watcher.test.ts 2>&1 → Instead: Check command syntax, file paths, and prerequisites before running
16
17
  - Avoid: Command failed: npx jest tests/unit/memory-sync-hook.test.ts 2>&1 → Instead: Check command syntax, file paths, and prerequisites before running
17
18
  - Avoid: Command failed: npm whoami 2>&1 && npm config get registry 2>&1 → Instead: Check command syntax, file paths, and prerequisites before running
18
19
  - npm install -g claude-recall@0.15.36 failed with notarget error
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "topicId": "failure-lessons",
3
- "sourceHash": "e099bbcfc9aa57afd1fb83ab4082a08ff820dd4f1dc31af6d4fbcfa3ad0af3d5",
4
- "memoryCount": 24,
5
- "generatedAt": "2026-03-17T09:43:28.755Z",
3
+ "sourceHash": "81c103e377f0523953c7512cbd3e06f5773ac5d57ddb8ec52e23b1effea1d1fe",
4
+ "memoryCount": 25,
5
+ "generatedAt": "2026-03-20T09:33:07.603Z",
6
6
  "memoryKeys": [
7
7
  "hook_failure_1772637584921_0tj4rrxnt",
8
+ "hook_failure_non-zero-exit_1773784543398_siidzp55g",
8
9
  "hook_failure_non-zero-exit_1773669669860_9mcdbx51y",
9
10
  "hook_failure_non-zero-exit_1773409269877_ful451241",
10
11
  "hook_failure_1773410916808_5k1r6zo4u",
@@ -8,10 +8,15 @@ source: claude-recall
8
8
 
9
9
  # Preferences
10
10
 
11
- Auto-generated from 91 memories. Last updated: 2026-03-17.
11
+ Auto-generated from 98 memories. Last updated: 2026-03-20.
12
12
 
13
13
  ## Rules
14
14
 
15
+ - Session test preference 1773999187736
16
+ - Test preference 1773999187658-2
17
+ - Test preference 1773999187658-1
18
+ - Test preference 1773999187658-0
19
+ - Test memory content
15
20
  - Session test preference 1773740608856
16
21
  - Test preference 1773740608797-2
17
22
  - Test preference 1773740608797-1
@@ -93,6 +98,7 @@ Auto-generated from 91 memories. Last updated: 2026-03-17.
93
98
  - Test memory 1770503266492-2
94
99
  - Test memory 1770503266492-1
95
100
  - Test memory 1770503266492-0
101
+ - Do not bump version numbers without asking first
96
102
  - Use local node_modules/.bin/claude-recall path in setup --install hook commands instead of npx to avoid registry hits
97
103
  - Use YYYYMMDD_descriptive_name.md naming convention for markdown files
98
104
  - When user says 'jam', it means 'just answer me' — a shorthand directive to provide direct answers without elaboration
@@ -103,6 +109,7 @@ Auto-generated from 91 memories. Last updated: 2026-03-17.
103
109
  - always use tabs
104
110
  - Do not change TTL settings—current configuration was deliberately chosen for a reason
105
111
  - Do not change TTL settings; current configuration is intentional and should be preserved
112
+ - you to replace existing install instructions on project readme with the above
106
113
 
107
114
  ---
108
115
  *Auto-generated by Claude Recall. Regenerate: `npx claude-recall skills generate`*
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "topicId": "preferences",
3
- "sourceHash": "e2c0f6c21a82d06422afe3b9bddeca1a865d50a9472e3868459612589dd9f5e9",
4
- "memoryCount": 91,
5
- "generatedAt": "2026-03-17T09:43:28.867Z",
3
+ "sourceHash": "e48e55f7de632696ce32c04f23c2e470a590547bb8541501214650efe670496e",
4
+ "memoryCount": 98,
5
+ "generatedAt": "2026-03-20T09:33:07.756Z",
6
6
  "memoryKeys": [
7
+ "memory_1773999187737_qrdwbas47",
8
+ "memory_1773999187696_gymxhasno",
9
+ "memory_1773999187680_p6rxtp38v",
10
+ "memory_1773999187660_0at06kilq",
11
+ "memory_1773999187564_d7j4ivfjo",
7
12
  "memory_1773740608857_7954nxhke",
8
13
  "memory_1773740608828_pkci7qhtt",
9
14
  "memory_1773740608815_k3tj4psf1",
@@ -85,6 +90,7 @@
85
90
  "memory_1770503266534_iq6pvjqj6",
86
91
  "memory_1770503266513_a1nxjz4j2",
87
92
  "memory_1770503266494_f3p8e15xv",
93
+ "hook_preference_1773743105207_791mk6w14",
88
94
  "hook_preference_1772641938714_ucidifq0v",
89
95
  "hook_preference_1772633980785_n9bwj39q4",
90
96
  "hook_preference_1771934420257_l4pawjgn5",
@@ -94,6 +100,7 @@
94
100
  "hook_preference_1771112104493_d3nz1o2ye",
95
101
  "hook_preference_1771112075356_3gh724ucm",
96
102
  "hook_preference_1773220761159_igi8x4tax",
97
- "hook_preference_1773220748369_qpl2lvvpd"
103
+ "hook_preference_1773220748369_qpl2lvvpd",
104
+ "hook_preference_1773784604998_geftlce1x"
98
105
  ]
99
106
  }
package/README.md CHANGED
@@ -14,6 +14,7 @@ Your preferences, project structure, workflows, corrections, and coding style ar
14
14
  - **Smart Memory Capture** — LLM-powered classification (via Claude Haiku) detects preferences and corrections from natural language, with silent regex fallback
15
15
  - **Project-Scoped Knowledge** — each project gets its own memory namespace; switch projects and Claude switches context automatically
16
16
  - **Failure Learning** — captures what failed, why, and what to do instead — so Claude doesn't repeat mistakes
17
+ - **Outcome-Aware Learning** — tracks action outcomes (bash results, test cycles, user corrections), synthesizes candidate lessons, and promotes validated patterns into active rules automatically
17
18
  - **Skill Crystallization** — when enough memories accumulate, auto-generates `.claude/skills/auto-*/` files that load natively without tool calls
18
19
  - **Local-Only** — SQLite on your machine, no telemetry, no cloud, works fully offline
19
20
 
@@ -74,9 +75,11 @@ Once installed, Claude Recall works automatically in the background:
74
75
 
75
76
  1. **First prompt** — the `search_enforcer` hook ensures Claude loads your stored rules before taking any action
76
77
  2. **As you work** — the `correction-detector` hook classifies every prompt you type. Natural statements like *"we use tabs here"* or *"no, put tests in `__tests__/`"* are detected and stored automatically
77
- 3. **End of turn** — the `memory-stop` hook scans recent transcript entries for corrections, preferences, failures, and devops patterns
78
- 4. **Before context compression** — the `precompact-preserve` hook sweeps up to 50 entries so nothing important is lost when the context window shrinks
79
- 5. **Rules sync to auto-memory** — the `memory-sync` hook exports active rules to `~/.claude/projects/{project}/memory/recall-rules.md` so they're available even when the MCP server is down
78
+ 3. **End of turn** — the `memory-stop` hook scans recent transcript entries for corrections, preferences, failures, and devops patterns. It also creates **episodes** summarizing the session outcome, generates **candidate lessons** from detected failures, and runs a **promotion cycle** to graduate validated patterns into active rules
79
+ 4. **Bash failures** — the `bash-failure-watcher` hook captures command failures in real-time, pairs successful fixes, and writes **outcome events** for the learning pipeline
80
+ 5. **Reask detection** — the `correction-detector` hook detects user frustration signals ("still broken", "that didn't work") and records them as outcome events
81
+ 6. **Before context compression** — the `precompact-preserve` hook sweeps up to 50 entries so nothing important is lost when the context window shrinks
82
+ 7. **Rules sync to auto-memory** — the `memory-sync` hook exports active rules to `~/.claude/projects/{project}/memory/recall-rules.md` so they're available even when the MCP server is down
80
83
 
81
84
  All classification uses Claude Haiku (via `ANTHROPIC_API_KEY` from your Claude Code session) with silent regex fallback. No configuration needed.
82
85
 
@@ -102,6 +105,20 @@ Claude Recall runs as an MCP server exposing four tools, backed by a local SQLit
102
105
  | `search_memory` | Search memories by keyword, ranked by relevance |
103
106
  | `delete_memory` | Delete a specific memory by ID |
104
107
 
108
+ ### Outcome-Aware Learning (v0.18.0)
109
+
110
+ Claude Recall tracks what happens *after* Claude acts — not just what was said. The outcome processing pipeline:
111
+
112
+ ```
113
+ action → outcome event → episode → candidate lesson → promotion → active rule
114
+ ```
115
+
116
+ - **Outcome events** capture bash results, test outcomes, user corrections, and reask signals
117
+ - **Episodes** summarize entire sessions with outcome type, severity, and confidence
118
+ - **Candidate lessons** are extracted from failure patterns — deduplicated by Jaccard similarity
119
+ - **Promotion engine** graduates lessons into active rules after 2+ observations (or immediately for high-severity failures), and demotes never-helpful memories
120
+ - **Outcome-aware retrieval** boosts memories with evidence, penalizes stale/unhelpful ones
121
+
105
122
  ---
106
123
 
107
124
  ## CLI Reference
@@ -134,6 +134,24 @@ class MemoryRetrieval {
134
134
  // Strength-based boost (replaces ad-hoc access_count and last_accessed boosts)
135
135
  const strength = MemoryRetrieval.computeStrength(memory);
136
136
  score *= 1 + strength * 2.0; // Up to 3x boost for max-strength memories
137
+ // Evidence boost: promoted lessons seen multiple times
138
+ const evidenceCount = this.getEvidenceCount(memory);
139
+ if (evidenceCount > 1) {
140
+ score *= 1 + Math.min(evidenceCount, 5) * 0.15; // up to 1.75x
141
+ }
142
+ // Helpfulness prior: retrieved + confirmed helpful
143
+ const stats = this.getMemoryStats(memory.key);
144
+ if (stats && stats.times_retrieved > 0) {
145
+ const helpRatio = stats.times_helpful / stats.times_retrieved;
146
+ score *= 0.8 + helpRatio * 0.4; // 0.8x to 1.2x
147
+ }
148
+ // Staleness penalty: old with no recent confirmation
149
+ if (stats?.last_confirmed_at) {
150
+ const daysSinceConfirmed = (Date.now() - new Date(stats.last_confirmed_at).getTime()) / 86400000;
151
+ if (daysSinceConfirmed > 30) {
152
+ score *= Math.max(0.5, 1 - (daysSinceConfirmed - 30) / 180);
153
+ }
154
+ }
137
155
  return score;
138
156
  }
139
157
  /**
@@ -189,6 +207,24 @@ class MemoryRetrieval {
189
207
  const timeDecay = Math.pow(0.5, daysSinceTouch / classConfig.halfLife);
190
208
  return Math.min(signalScore * timeDecay, 1.0);
191
209
  }
210
+ getMemoryStats(key) {
211
+ try {
212
+ const row = this.storage.getDatabase().prepare('SELECT times_retrieved, times_helpful, last_confirmed_at FROM memory_stats WHERE memory_key = ?').get(key);
213
+ return row || null;
214
+ }
215
+ catch {
216
+ return null;
217
+ }
218
+ }
219
+ getEvidenceCount(memory) {
220
+ try {
221
+ const row = this.storage.getDatabase().prepare("SELECT evidence_count FROM candidate_lessons WHERE promoted_memory_key = ? AND status = 'promoted' LIMIT 1").get(memory.key);
222
+ return row?.evidence_count || 0;
223
+ }
224
+ catch {
225
+ return 0;
226
+ }
227
+ }
192
228
  searchByKeyword(keyword) {
193
229
  const results = this.storage.search(keyword);
194
230
  const context = { timestamp: Date.now() };
@@ -53,6 +53,7 @@ const path = __importStar(require("path"));
53
53
  const os = __importStar(require("os"));
54
54
  const shared_1 = require("./shared");
55
55
  const memory_1 = require("../services/memory");
56
+ const outcome_storage_1 = require("../services/outcome-storage");
56
57
  const HOOK_NAME = 'bash-failure-watcher';
57
58
  const EXIT_CODE_REGEX = /Exit code (\d+)/;
58
59
  const MAX_PENDING = 5;
@@ -152,6 +153,21 @@ async function handleFailure(command, exitCode, output, sessionId) {
152
153
  context: { timestamp: Date.now() },
153
154
  relevanceScore: 0.85,
154
155
  });
156
+ // Write outcome event for outcome-aware learning
157
+ try {
158
+ const outcomeStorage = outcome_storage_1.OutcomeStorage.getInstance();
159
+ outcomeStorage.createOutcomeEvent({
160
+ event_type: 'bash_result',
161
+ actor: 'tool',
162
+ action_summary: `Bash: ${truncate(command, 100)}`,
163
+ next_state_summary: `Exit code ${exitCode}: ${truncate(firstLine(output), 150)}`,
164
+ exit_code: parseInt(exitCode),
165
+ tags: extractCommandTags(command),
166
+ });
167
+ }
168
+ catch (err) {
169
+ (0, shared_1.hookLog)(HOOK_NAME, `Outcome event error: ${err}`);
170
+ }
155
171
  (0, shared_1.hookLog)(HOOK_NAME, `Stored failure: ${truncate(command, 60)} (exit ${exitCode})`);
156
172
  // Track pending failure for fix pairing
157
173
  const pending = readPendingFailures(sessionId);
@@ -186,6 +202,21 @@ async function handleSuccess(command, sessionId) {
186
202
  what_should_do: `Fix: ${truncate(command, 200)}`,
187
203
  },
188
204
  });
205
+ // Write positive outcome event for the fix
206
+ try {
207
+ const outcomeStorage = outcome_storage_1.OutcomeStorage.getInstance();
208
+ outcomeStorage.createOutcomeEvent({
209
+ event_type: 'bash_result',
210
+ actor: 'tool',
211
+ action_summary: `Fix: ${truncate(command, 100)}`,
212
+ next_state_summary: `Success after previous failure: ${truncate(pf.command, 100)}`,
213
+ exit_code: 0,
214
+ tags: extractCommandTags(command),
215
+ });
216
+ }
217
+ catch (err) {
218
+ (0, shared_1.hookLog)(HOOK_NAME, `Positive outcome event error: ${err}`);
219
+ }
189
220
  (0, shared_1.hookLog)(HOOK_NAME, `Paired fix: "${truncate(command, 60)}" → ${pf.memoryKey}`);
190
221
  matched = true;
191
222
  // Don't add to remaining — consumed
@@ -201,3 +232,22 @@ async function handleSuccess(command, sessionId) {
201
232
  }
202
233
  writePendingFailures(sessionId, remaining);
203
234
  }
235
+ /**
236
+ * Extract command tags from a bash command for outcome tagging.
237
+ * e.g., "npm test -- --coverage" → ["npm", "test"]
238
+ */
239
+ function extractCommandTags(command) {
240
+ const parts = command.trim().split(/\s+/).filter(Boolean);
241
+ // Take first 2 meaningful tokens (skip flags starting with -)
242
+ const tags = [];
243
+ for (const part of parts) {
244
+ if (part.startsWith('-'))
245
+ continue;
246
+ if (part.includes('/') && part.length > 20)
247
+ continue; // Skip long paths
248
+ tags.push(part.toLowerCase());
249
+ if (tags.length >= 2)
250
+ break;
251
+ }
252
+ return tags;
253
+ }
@@ -9,6 +9,15 @@
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.handleCorrectionDetector = handleCorrectionDetector;
11
11
  const shared_1 = require("./shared");
12
+ const outcome_storage_1 = require("../services/outcome-storage");
13
+ const REASK_PATTERNS = [
14
+ /still broken/i,
15
+ /that'?s not what I (?:meant|asked|wanted)/i,
16
+ /wrong file/i,
17
+ /try again/i,
18
+ /that didn'?t (?:work|fix|help)/i,
19
+ /you (?:missed|forgot|ignored)/i,
20
+ ];
12
21
  async function handleCorrectionDetector(input) {
13
22
  const prompt = input?.prompt ?? '';
14
23
  // Skip trivial / non-text input
@@ -16,6 +25,23 @@ async function handleCorrectionDetector(input) {
16
25
  return;
17
26
  if (prompt.startsWith('```') || prompt.startsWith('{'))
18
27
  return;
28
+ // Detect reask signals before classification
29
+ try {
30
+ for (const pattern of REASK_PATTERNS) {
31
+ if (pattern.test(prompt)) {
32
+ outcome_storage_1.OutcomeStorage.getInstance().createOutcomeEvent({
33
+ event_type: 'reask_signal',
34
+ actor: 'user',
35
+ next_state_summary: `User reask detected: ${prompt.substring(0, 100)}`,
36
+ tags: ['reask'],
37
+ });
38
+ break;
39
+ }
40
+ }
41
+ }
42
+ catch (err) {
43
+ (0, shared_1.hookLog)('correction-detector', `Reask signal detection error: ${err}`);
44
+ }
19
45
  const result = await (0, shared_1.classifyContent)(prompt);
20
46
  if (!result)
21
47
  return;
@@ -7,6 +7,7 @@
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.classifyWithLLM = classifyWithLLM;
10
+ exports.extractHindsightHint = extractHindsightHint;
10
11
  exports.classifyBatchWithLLM = classifyBatchWithLLM;
11
12
  // Lazy singleton — avoid import cost when API key is absent
12
13
  let clientInstance; // undefined = not yet checked
@@ -120,6 +121,33 @@ async function classifyWithLLM(text) {
120
121
  * Returns an array of results (null for unclassifiable items).
121
122
  * Falls back to null array on total failure (caller should use regex).
122
123
  */
124
+ async function extractHindsightHint(failureDescription, context) {
125
+ const client = getClient();
126
+ if (!client)
127
+ return null;
128
+ try {
129
+ const response = await client.messages.create({
130
+ model: MODEL,
131
+ max_tokens: 300,
132
+ system: 'You extract actionable hindsight lessons from failures. Given a failure description and context, produce a JSON object with: hint_text (imperative rule to prevent recurrence), hint_kind (one of: rule, preference, anti_pattern, workflow, debug_fix, failure_preventer), applies_when (array of 1-3 situation tags). Respond with ONLY valid JSON.',
133
+ messages: [{ role: 'user', content: `Failure: ${failureDescription}\nContext: ${context}` }],
134
+ });
135
+ const content = response.content?.[0];
136
+ if (content?.type !== 'text')
137
+ return null;
138
+ const result = parseJSON(content.text);
139
+ if (!result.hint_text || !result.hint_kind)
140
+ return null;
141
+ return {
142
+ hint_text: result.hint_text,
143
+ hint_kind: result.hint_kind,
144
+ applies_when: Array.isArray(result.applies_when) ? result.applies_when : [],
145
+ };
146
+ }
147
+ catch {
148
+ return null;
149
+ }
150
+ }
123
151
  async function classifyBatchWithLLM(texts) {
124
152
  if (texts.length === 0)
125
153
  return [];
@@ -6,12 +6,46 @@
6
6
  * Reads the last 6 transcript entries, classifies them,
7
7
  * dedup-checks, and stores up to 3 new memories.
8
8
  */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
9
42
  Object.defineProperty(exports, "__esModule", { value: true });
10
43
  exports.handleMemoryStop = handleMemoryStop;
11
44
  const shared_1 = require("./shared");
12
45
  const memory_1 = require("../services/memory");
13
46
  const config_1 = require("../services/config");
14
47
  const failure_detectors_1 = require("./failure-detectors");
48
+ const outcome_storage_1 = require("../services/outcome-storage");
15
49
  const MAX_STORE = 3;
16
50
  async function handleMemoryStop(input) {
17
51
  const transcriptPath = input?.transcript_path ?? '';
@@ -24,6 +58,14 @@ async function handleMemoryStop(input) {
24
58
  (0, shared_1.hookLog)('memory-stop', 'No transcript entries found');
25
59
  return;
26
60
  }
61
+ // Create an episode for this session
62
+ const outcomeStorage = outcome_storage_1.OutcomeStorage.getInstance();
63
+ const projectId = config_1.ConfigService.getInstance().getProjectId();
64
+ const episodeId = outcomeStorage.createEpisode({
65
+ project_id: projectId,
66
+ session_id: input?.session_id,
67
+ source: 'memory-stop',
68
+ });
27
69
  // Extract user-only texts, filter, then batch-classify in one API call
28
70
  const textsWithIndex = [];
29
71
  for (let i = 0; i < entries.length; i++) {
@@ -68,7 +110,25 @@ async function handleMemoryStop(input) {
68
110
  // Scan for citations in assistant messages to track compliance
69
111
  scanForCitations(transcriptPath);
70
112
  // Scan transcript for failure signals (non-zero exits, test cycles, backtracking, etc.)
71
- detectAndStoreFailures(transcriptPath);
113
+ const failures = detectAndStoreFailures(transcriptPath, episodeId);
114
+ outcomeStorage.updateEpisode(episodeId, {
115
+ outcome_type: failures.length > 0 ? 'failure' : 'success',
116
+ severity: failures.length > 0 ? 'medium' : 'low',
117
+ outcome_summary: `${stored} memories, ${failures.length} failures`,
118
+ });
119
+ // Generate candidate lessons from high-confidence failures
120
+ generateCandidateLessons(failures, episodeId, projectId);
121
+ // Run promotion cycle
122
+ try {
123
+ const { PromotionEngine } = await Promise.resolve().then(() => __importStar(require('../services/promotion-engine')));
124
+ const result = PromotionEngine.getInstance().runCycle(projectId);
125
+ if (result.promoted > 0 || result.archived > 0) {
126
+ (0, shared_1.hookLog)('memory-stop', `Promotion: ${result.promoted} promoted, ${result.archived} archived`);
127
+ }
128
+ }
129
+ catch (err) {
130
+ (0, shared_1.hookLog)('memory-stop', `Promotion error: ${err}`);
131
+ }
72
132
  }
73
133
  /**
74
134
  * Scan transcript for (applied from memory: ...) citations and increment cite_count.
@@ -125,6 +185,10 @@ function scanForCitations(transcriptPath) {
125
185
  (0, shared_1.hookLog)('memory-stop', `Best match: "${bestContent}" containment=${bestScore.toFixed(3)} key=${bestKey}`);
126
186
  if (bestScore >= 0.5) {
127
187
  memoryService.incrementCiteCount(bestKey);
188
+ try {
189
+ outcome_storage_1.OutcomeStorage.getInstance().recordHelpful(bestKey);
190
+ }
191
+ catch { }
128
192
  (0, shared_1.hookLog)('memory-stop', `Citation matched: "${cite.substring(0, 50)}" → rule ${bestKey} (containment=${bestScore.toFixed(3)})`);
129
193
  }
130
194
  else {
@@ -187,17 +251,17 @@ function extractRuleContent(value) {
187
251
  /**
188
252
  * Scan the last 200 transcript entries for failure signals and store up to 3.
189
253
  */
190
- function detectAndStoreFailures(transcriptPath) {
254
+ function detectAndStoreFailures(transcriptPath, episodeId) {
191
255
  try {
192
256
  const entries = (0, shared_1.readTranscriptTail)(transcriptPath, 200);
193
257
  if (entries.length === 0) {
194
258
  (0, shared_1.hookLog)('memory-stop', '[FailureDetector] No entries to scan');
195
- return;
259
+ return [];
196
260
  }
197
261
  const failures = (0, failure_detectors_1.detectTranscriptFailures)(entries);
198
262
  if (failures.length === 0) {
199
263
  (0, shared_1.hookLog)('memory-stop', '[FailureDetector] No failure signals detected');
200
- return;
264
+ return [];
201
265
  }
202
266
  (0, shared_1.hookLog)('memory-stop', `[FailureDetector] Detected ${failures.length} failure signal(s)`);
203
267
  const projectId = config_1.ConfigService.getInstance().getProjectId();
@@ -230,8 +294,56 @@ function detectAndStoreFailures(transcriptPath) {
230
294
  (0, shared_1.hookLog)('memory-stop', `[FailureDetector] Stored ${failure.signal}: ${failure.content.what_failed.substring(0, 80)}`);
231
295
  }
232
296
  (0, shared_1.hookLog)('memory-stop', `[FailureDetector] Stored ${stored} failure(s) from ${failures.length} detected`);
297
+ return failures;
233
298
  }
234
299
  catch (error) {
235
300
  (0, shared_1.hookLog)('memory-stop', `[FailureDetector] Error: ${error}`);
301
+ return [];
302
+ }
303
+ }
304
+ /**
305
+ * Generate candidate lessons from high-confidence failures.
306
+ * Deduplicates against existing lessons and increments evidence count for similar ones.
307
+ */
308
+ function generateCandidateLessons(failures, episodeId, projectId) {
309
+ try {
310
+ const outcomeStorage = outcome_storage_1.OutcomeStorage.getInstance();
311
+ for (const f of failures) {
312
+ if (f.confidence < 0.7)
313
+ continue;
314
+ const similar = outcomeStorage.findSimilarLessons(f.content.what_should_do, projectId);
315
+ if (similar.length > 0) {
316
+ outcomeStorage.incrementEvidenceCount(similar[0].id);
317
+ }
318
+ else {
319
+ outcomeStorage.createCandidateLesson({
320
+ project_id: projectId,
321
+ episode_id: episodeId,
322
+ lesson_text: f.content.what_should_do,
323
+ lesson_kind: 'failure_preventer',
324
+ applies_when: extractTagsFromContext(f.content.context),
325
+ outcome_type: 'negative',
326
+ reward_band: -1,
327
+ confidence: f.confidence,
328
+ durability: 'project',
329
+ });
330
+ }
331
+ }
332
+ }
333
+ catch (err) {
334
+ (0, shared_1.hookLog)('memory-stop', `Candidate lesson generation error: ${err}`);
335
+ }
336
+ }
337
+ function extractTagsFromContext(context) {
338
+ const tags = [];
339
+ const words = context.toLowerCase().split(/\s+/).filter(w => w.length >= 4);
340
+ // Take up to 5 significant words as tags
341
+ for (const w of words) {
342
+ if (tags.length >= 5)
343
+ break;
344
+ if (!['that', 'this', 'with', 'from', 'were', 'been'].includes(w)) {
345
+ tags.push(w);
346
+ }
236
347
  }
348
+ return tags;
237
349
  }
@@ -4,6 +4,7 @@ exports.MemoryTools = void 0;
4
4
  const config_1 = require("../../services/config");
5
5
  const search_monitor_1 = require("../../services/search-monitor");
6
6
  const skill_generator_1 = require("../../services/skill-generator");
7
+ const outcome_storage_1 = require("../../services/outcome-storage");
7
8
  class MemoryTools {
8
9
  constructor(memoryService, logger) {
9
10
  this.memoryService = memoryService;
@@ -294,6 +295,17 @@ class MemoryTools {
294
295
  ]);
295
296
  // Record to SearchMonitor so monitoring/stats still work
296
297
  this.searchMonitor.recordSearch('load_rules', totalRules, context.sessionId, 'mcp', { tool: 'load_rules', tokenMetrics: { resultTokens, tokensSaved: totalRules > 0 ? totalRules * 200 : 0 } });
298
+ // Track retrievals for outcome-aware scoring
299
+ try {
300
+ const outcomeStorage = outcome_storage_1.OutcomeStorage.getInstance();
301
+ const allMemories = [...rules.preferences, ...rules.corrections, ...rules.failures, ...rules.devops];
302
+ for (const m of allMemories) {
303
+ outcomeStorage.recordRetrieval(m.key);
304
+ }
305
+ }
306
+ catch {
307
+ // Non-critical
308
+ }
297
309
  let rulesText;
298
310
  if (sections.length > 0) {
299
311
  const body = sections.join('\n\n');
@@ -341,6 +353,16 @@ class MemoryTools {
341
353
  }
342
354
  const results = this.memoryService.findRelevant(searchContext);
343
355
  const topResults = results.slice(0, limit);
356
+ // Track retrievals in memory_stats
357
+ try {
358
+ const outcomeStorage = outcome_storage_1.OutcomeStorage.getInstance();
359
+ for (const r of topResults) {
360
+ outcomeStorage.recordRetrieval(r.key);
361
+ }
362
+ }
363
+ catch {
364
+ // Non-critical — don't fail the search
365
+ }
344
366
  // Record to SearchMonitor
345
367
  this.searchMonitor.recordSearch(query, topResults.length, context.sessionId, 'mcp', { tool: 'search_memory', type: type || 'all' });
346
368
  const formatted = topResults.map(r => {