claude-recall 0.17.1 → 0.18.0

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
  }
@@ -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 => {
@@ -79,6 +79,8 @@ class MemoryStorage {
79
79
  throw new Error(`Failed to initialize database: ${error}`);
80
80
  }
81
81
  }
82
+ // Run migrations for new tables (v0.18.0+) on fresh databases too
83
+ this.migrateSchema();
82
84
  }
83
85
  /**
84
86
  * Migrate existing database schema to add missing columns
@@ -137,6 +139,73 @@ class MemoryStorage {
137
139
  console.log('✅ Added content_hash column');
138
140
  }
139
141
  }
142
+ // v0.18.0: Outcome-aware learning tables
143
+ const outcomeTables = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('episodes','outcome_events','candidate_lessons','memory_stats')").all();
144
+ const existingOutcomeTables = new Set(outcomeTables.map(t => t.name));
145
+ if (!existingOutcomeTables.has('episodes')) {
146
+ this.db.exec(`CREATE TABLE episodes (
147
+ id TEXT PRIMARY KEY,
148
+ project_id TEXT NOT NULL,
149
+ session_id TEXT,
150
+ started_at TEXT NOT NULL,
151
+ ended_at TEXT,
152
+ task_summary TEXT,
153
+ action_summary TEXT,
154
+ outcome_summary TEXT,
155
+ outcome_type TEXT,
156
+ severity TEXT,
157
+ confidence REAL,
158
+ source TEXT,
159
+ created_at TEXT NOT NULL
160
+ )`);
161
+ }
162
+ if (!existingOutcomeTables.has('outcome_events')) {
163
+ this.db.exec(`CREATE TABLE outcome_events (
164
+ id TEXT PRIMARY KEY,
165
+ episode_id TEXT,
166
+ event_type TEXT NOT NULL,
167
+ actor TEXT NOT NULL,
168
+ action_summary TEXT,
169
+ next_state_summary TEXT NOT NULL,
170
+ exit_code INTEGER,
171
+ tags_json TEXT,
172
+ created_at TEXT NOT NULL
173
+ )`);
174
+ this.db.exec('CREATE INDEX idx_outcome_events_episode ON outcome_events(episode_id)');
175
+ this.db.exec('CREATE INDEX idx_outcome_events_type ON outcome_events(event_type)');
176
+ }
177
+ if (!existingOutcomeTables.has('candidate_lessons')) {
178
+ this.db.exec(`CREATE TABLE candidate_lessons (
179
+ id TEXT PRIMARY KEY,
180
+ project_id TEXT NOT NULL,
181
+ episode_id TEXT,
182
+ lesson_text TEXT NOT NULL,
183
+ lesson_kind TEXT NOT NULL,
184
+ applies_when_json TEXT,
185
+ outcome_type TEXT NOT NULL,
186
+ reward_band INTEGER,
187
+ confidence REAL NOT NULL,
188
+ durability TEXT NOT NULL,
189
+ evidence_count INTEGER DEFAULT 1,
190
+ status TEXT NOT NULL DEFAULT 'candidate',
191
+ promoted_memory_key TEXT,
192
+ created_at TEXT NOT NULL,
193
+ updated_at TEXT NOT NULL
194
+ )`);
195
+ this.db.exec('CREATE INDEX idx_candidate_lessons_status ON candidate_lessons(status)');
196
+ this.db.exec('CREATE INDEX idx_candidate_lessons_project ON candidate_lessons(project_id)');
197
+ }
198
+ if (!existingOutcomeTables.has('memory_stats')) {
199
+ this.db.exec(`CREATE TABLE memory_stats (
200
+ memory_key TEXT PRIMARY KEY,
201
+ times_observed INTEGER DEFAULT 1,
202
+ times_retrieved INTEGER DEFAULT 0,
203
+ times_helpful INTEGER DEFAULT 0,
204
+ times_unhelpful INTEGER DEFAULT 0,
205
+ last_confirmed_at TEXT,
206
+ last_retrieved_at TEXT
207
+ )`);
208
+ }
140
209
  }
141
210
  catch (error) {
142
211
  console.error('⚠️ Schema migration error:', error);