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.
- package/.claude/skills/auto-corrections/SKILL.md +2 -1
- package/.claude/skills/auto-corrections/manifest.json +4 -3
- package/.claude/skills/auto-failure-lessons/SKILL.md +2 -1
- package/.claude/skills/auto-failure-lessons/manifest.json +4 -3
- package/.claude/skills/auto-preferences/SKILL.md +8 -1
- package/.claude/skills/auto-preferences/manifest.json +11 -4
- package/dist/core/retrieval.js +36 -0
- package/dist/hooks/bash-failure-watcher.js +50 -0
- package/dist/hooks/correction-detector.js +26 -0
- package/dist/hooks/llm-classifier.js +28 -0
- package/dist/hooks/memory-stop-hook.js +116 -4
- package/dist/mcp/tools/memory-tools.js +22 -0
- package/dist/memory/storage.js +69 -0
- package/dist/services/outcome-storage.js +214 -0
- package/dist/services/promotion-engine.js +168 -0
- package/docs/OpenClaw-RL/claude-recall-v0.18.0-post-action-learning-layer.md +589 -0
- package/docs/OpenClaw-RL/ideas-from-openclaw-rl.md +440 -0
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@ source: claude-recall
|
|
|
8
8
|
|
|
9
9
|
# Corrections
|
|
10
10
|
|
|
11
|
-
Auto-generated from
|
|
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": "
|
|
4
|
-
"memoryCount":
|
|
5
|
-
"generatedAt": "2026-03-
|
|
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
|
|
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": "
|
|
4
|
-
"memoryCount":
|
|
5
|
-
"generatedAt": "2026-03-
|
|
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
|
|
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": "
|
|
4
|
-
"memoryCount":
|
|
5
|
-
"generatedAt": "2026-03-
|
|
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/dist/core/retrieval.js
CHANGED
|
@@ -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 => {
|
package/dist/memory/storage.js
CHANGED
|
@@ -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);
|