@yamo/memory-mesh 3.1.1 → 3.1.3
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/README.md +3 -3
- package/bin/memory_mesh.js +122 -4
- package/bin/setup.js +3 -0
- package/lib/memory/memory-mesh.d.ts +110 -1
- package/lib/memory/memory-mesh.js +306 -1
- package/lib/memory/memory-mesh.ts +393 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# MemoryMesh (
|
|
1
|
+
# MemoryMesh (Singularity Edition)
|
|
2
2
|
|
|
3
3
|
Portable, semantic memory system for AI agents with automatic Layer 0 sanitization and **Autonomous Kernel Integration**.
|
|
4
4
|
|
|
@@ -7,7 +7,7 @@ Built on the [YAMO Protocol](https://github.com/yamo-protocol) for transparent a
|
|
|
7
7
|
## 🌌 Singularity Edition Highlights
|
|
8
8
|
|
|
9
9
|
- **Intelligent Installer**: Automatically detects OpenClaw workspaces and performs a full kernel upgrade.
|
|
10
|
-
- **YAMO Unified OS
|
|
10
|
+
- **YAMO Unified OS**: Natively includes the complete Macro (Specification) and Micro (Execution) workflow suite.
|
|
11
11
|
- **Ghost Protection**: Self-healing `AGENTS.md` injection to prevent narrative drift and maintain cognitive alignment.
|
|
12
12
|
- **Autonomous Bootstrap**: Deploys `BOOTSTRAP.yamo` as the primary agent entry point for protocol-native execution.
|
|
13
13
|
- **Surgical Deployment**: Intelligently skips global CLI clutter when working in project-specific modes.
|
|
@@ -33,7 +33,7 @@ npm install @yamo/memory-mesh
|
|
|
33
33
|
|
|
34
34
|
### 🚀 Singularity Setup (OpenClaw)
|
|
35
35
|
|
|
36
|
-
To upgrade your workspace to
|
|
36
|
+
To upgrade your workspace to full fidelity:
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
39
|
npx memory-mesh-setup
|
package/bin/memory_mesh.js
CHANGED
|
@@ -56,11 +56,12 @@ program
|
|
|
56
56
|
// 2. Directory Ingest Command
|
|
57
57
|
program
|
|
58
58
|
.command('ingest-dir')
|
|
59
|
-
.
|
|
59
|
+
.alias('pull')
|
|
60
|
+
.description('Recursively ingest a directory of files (Smart Ingest)')
|
|
60
61
|
.argument('<path>', 'Directory path to ingest')
|
|
61
|
-
.option('-e, --extension <ext>', 'Filter by file extension
|
|
62
|
-
.option('-t, --type <type>', 'Memory type
|
|
63
|
-
.option('-r, --recursive', 'Ingest subdirectories',
|
|
62
|
+
.option('-e, --extension <ext>', 'Filter by file extension', '')
|
|
63
|
+
.option('-t, --type <type>', 'Memory type', 'documentation')
|
|
64
|
+
.option('-r, --recursive', 'Ingest subdirectories', true)
|
|
64
65
|
.action(async (dirPath, options) => {
|
|
65
66
|
const mesh = new MemoryMesh();
|
|
66
67
|
try {
|
|
@@ -155,4 +156,121 @@ program
|
|
|
155
156
|
}
|
|
156
157
|
});
|
|
157
158
|
|
|
159
|
+
// 4. Get Command
|
|
160
|
+
program
|
|
161
|
+
.command('get')
|
|
162
|
+
.description('Retrieve a single memory by ID')
|
|
163
|
+
.requiredOption('--id <id>', 'Memory record ID')
|
|
164
|
+
.action(async (options) => {
|
|
165
|
+
const mesh = new MemoryMesh();
|
|
166
|
+
try {
|
|
167
|
+
await mesh.init();
|
|
168
|
+
const record = await mesh.get(options.id);
|
|
169
|
+
if (!record) {
|
|
170
|
+
process.stdout.write(`[MemoryMesh] No record found with id: ${options.id}\n`);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
const meta = typeof record.metadata === 'string' ? JSON.parse(record.metadata) : record.metadata;
|
|
174
|
+
process.stdout.write(`[MemoryMesh] id: ${record.id}\n`);
|
|
175
|
+
process.stdout.write(`[MemoryMesh] content: ${record.content}\n`);
|
|
176
|
+
process.stdout.write(`[MemoryMesh] type: ${meta?.type ?? 'unknown'}\n`);
|
|
177
|
+
process.stdout.write(`[MemoryMesh] created_at: ${record.created_at}\n`);
|
|
178
|
+
process.stdout.write(`[MemoryMesh] metadata: ${JSON.stringify(meta, null, 2)}\n`);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
console.error(`❌ Error: ${err.message}`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
} finally {
|
|
183
|
+
await mesh.close();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// 5. Delete Command
|
|
188
|
+
program
|
|
189
|
+
.command('delete')
|
|
190
|
+
.description('Delete a memory by ID')
|
|
191
|
+
.requiredOption('--id <id>', 'Memory record ID to delete')
|
|
192
|
+
.action(async (options) => {
|
|
193
|
+
const mesh = new MemoryMesh();
|
|
194
|
+
try {
|
|
195
|
+
await mesh.init();
|
|
196
|
+
await mesh.delete(options.id);
|
|
197
|
+
process.stdout.write(`[MemoryMesh] Deleted record ${options.id}\n`);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
console.error(`❌ Error: ${err.message}`);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
} finally {
|
|
202
|
+
await mesh.close();
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// 6. Reflect Command
|
|
207
|
+
program
|
|
208
|
+
.command('reflect')
|
|
209
|
+
.description('Query distilled lessons from memory (wisdom distillation)')
|
|
210
|
+
.option('--topic <text>', 'Topic or query to reflect on', '')
|
|
211
|
+
.option('--lookback <n>', 'Limit results to this many lessons', '10')
|
|
212
|
+
.action(async (options) => {
|
|
213
|
+
const mesh = new MemoryMesh();
|
|
214
|
+
try {
|
|
215
|
+
await mesh.init();
|
|
216
|
+
const query = options.topic || 'lessons learned patterns errors fixes';
|
|
217
|
+
const limit = parseInt(options.lookback) || 10;
|
|
218
|
+
const lessons = await mesh.queryLessons(query, { limit });
|
|
219
|
+
if (lessons.length === 0) {
|
|
220
|
+
process.stdout.write(`[MemoryMesh] No lessons found${options.topic ? ` for topic: ${options.topic}` : ''}.\n`);
|
|
221
|
+
} else {
|
|
222
|
+
process.stdout.write(`[MemoryMesh] Reflecting on ${lessons.length} lesson(s):\n\n`);
|
|
223
|
+
for (const lesson of lessons) {
|
|
224
|
+
process.stdout.write(` scope: ${lesson.applicableScope}\n`);
|
|
225
|
+
process.stdout.write(` rule: ${lesson.preventativeRule}\n`);
|
|
226
|
+
process.stdout.write(` confidence: ${lesson.ruleConfidence}\n`);
|
|
227
|
+
process.stdout.write('\n');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} catch (err) {
|
|
231
|
+
console.error(`❌ Error: ${err.message}`);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
} finally {
|
|
234
|
+
await mesh.close();
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// S-MORA command (RFC-0012)
|
|
239
|
+
program
|
|
240
|
+
.command('smora')
|
|
241
|
+
.description('S-MORA enhanced retrieval: HyDE-Lite + multi-channel + heritage-aware reranking (RFC-0012)')
|
|
242
|
+
.argument('<query>', 'The retrieval query')
|
|
243
|
+
.option('-l, --limit <n>', 'Max results to return', '10')
|
|
244
|
+
.option('--no-hyde', 'Disable HyDE-Lite query expansion (Layer 1)')
|
|
245
|
+
.option('--intent <items>', 'Session intent chain for heritage bonus (comma-separated)', '')
|
|
246
|
+
.option('--json', 'Output raw JSON response')
|
|
247
|
+
.action(async (query, options) => {
|
|
248
|
+
const mesh = new MemoryMesh();
|
|
249
|
+
try {
|
|
250
|
+
await mesh.init();
|
|
251
|
+
const sessionIntent = options.intent
|
|
252
|
+
? options.intent.split(',').map((s) => s.trim()).filter(Boolean)
|
|
253
|
+
: [];
|
|
254
|
+
const resp = await mesh.smora(query, {
|
|
255
|
+
limit: parseInt(options.limit) || 10,
|
|
256
|
+
enableHyDE: options.hyde !== false,
|
|
257
|
+
sessionIntent,
|
|
258
|
+
});
|
|
259
|
+
if (options.json) {
|
|
260
|
+
process.stdout.write(JSON.stringify(resp, null, 2) + '\n');
|
|
261
|
+
} else {
|
|
262
|
+
const p = resp.pipeline;
|
|
263
|
+
process.stdout.write(`[S-MORA] ${resp.results.length} result(s) | HyDE:${p.queryExpanded} heritage:${p.heritageAware} latency:${p.latencyMs}ms\n\n`);
|
|
264
|
+
for (const r of resp.results) {
|
|
265
|
+
process.stdout.write(` [${r.rrfRank}] score:${r.score.toFixed(3)} | ${r.content.slice(0, 100)}${r.content.length > 100 ? '...' : ''}\n`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
} catch (err) {
|
|
269
|
+
console.error(`❌ Error: ${err.message}`);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
} finally {
|
|
272
|
+
await mesh.close();
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
158
276
|
program.parse();
|
package/bin/setup.js
CHANGED
|
@@ -262,6 +262,7 @@ async function installTools(env) {
|
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
function showUsage(env) {
|
|
265
|
+
const pkg = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf-8'));
|
|
265
266
|
log('\n✨ Setup Complete!', 'bright');
|
|
266
267
|
|
|
267
268
|
if (env.isOpenClaw) {
|
|
@@ -269,11 +270,13 @@ function showUsage(env) {
|
|
|
269
270
|
log(' • Ghost Protection active in AGENTS.md', 'cyan');
|
|
270
271
|
log(' • Kernel entry point: BOOTSTRAP.yamo', 'cyan');
|
|
271
272
|
log(' • Modules deployed to: yamo-native-agent/', 'cyan');
|
|
273
|
+
log(` • Version: ${pkg.version}`, 'cyan');
|
|
272
274
|
log(' • REFRESH SESSION to activate v3.0 fidelity.', 'cyan');
|
|
273
275
|
} else {
|
|
274
276
|
log('\n📚 STANDARD MODE:', 'blue');
|
|
275
277
|
log(' • Skills installed to Claude/Gemini', 'blue');
|
|
276
278
|
log(' • Use /yamo-super to start workflows', 'blue');
|
|
279
|
+
log(` • Version: ${pkg.version}`, 'blue');
|
|
277
280
|
}
|
|
278
281
|
|
|
279
282
|
log('\n🔧 Tools:', 'blue');
|
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
/** RFC-0012 S-MORA types */
|
|
2
|
+
export interface SMORAOptions {
|
|
3
|
+
limit?: number;
|
|
4
|
+
retrievalLimit?: number;
|
|
5
|
+
sessionIntent?: string[];
|
|
6
|
+
enableSynthesis?: boolean;
|
|
7
|
+
enableHyDE?: boolean;
|
|
8
|
+
useCache?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface SMORAResult {
|
|
11
|
+
id: string;
|
|
12
|
+
content: string;
|
|
13
|
+
metadata: Record<string, unknown>;
|
|
14
|
+
score: number;
|
|
15
|
+
semanticScore: number;
|
|
16
|
+
heritageBonus: number;
|
|
17
|
+
recencyDecay: number;
|
|
18
|
+
rrfRank: number;
|
|
19
|
+
}
|
|
20
|
+
export interface SMORAResponse {
|
|
21
|
+
results: SMORAResult[];
|
|
22
|
+
synthesis?: string;
|
|
23
|
+
pipeline: {
|
|
24
|
+
queryExpanded: boolean;
|
|
25
|
+
heritageAware: boolean;
|
|
26
|
+
synthesized: boolean;
|
|
27
|
+
latencyMs: number;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
1
30
|
/**
|
|
2
31
|
* MemoryMesh class for managing vector memory storage
|
|
3
32
|
*/
|
|
@@ -228,7 +257,11 @@ export declare class MemoryMesh {
|
|
|
228
257
|
* Note: YAMO emission is non-critical - failures are logged but don't throw
|
|
229
258
|
* to prevent disrupting the main operation.
|
|
230
259
|
*/
|
|
231
|
-
_emitYamoBlock(operationType: any, memoryId: any, yamoText: any
|
|
260
|
+
_emitYamoBlock(operationType: any, memoryId: any, yamoText: any, heritage?: {
|
|
261
|
+
intentChain: string[];
|
|
262
|
+
hypotheses: string[];
|
|
263
|
+
rationales: string[];
|
|
264
|
+
}): Promise<void>;
|
|
232
265
|
/**
|
|
233
266
|
* Search memory using hybrid vector + keyword search with Reciprocal Rank Fusion (RRF).
|
|
234
267
|
*
|
|
@@ -284,6 +317,82 @@ export declare class MemoryMesh {
|
|
|
284
317
|
created_at: any;
|
|
285
318
|
updated_at: any;
|
|
286
319
|
}>;
|
|
320
|
+
/**
|
|
321
|
+
* Delete a memory entry by ID.
|
|
322
|
+
*/
|
|
323
|
+
delete(id: string): Promise<void>;
|
|
324
|
+
/**
|
|
325
|
+
* Distill a LessonLearned block (RFC-0011 §3.5).
|
|
326
|
+
* Idempotent: same patternId + equal/higher confidence returns existing.
|
|
327
|
+
*/
|
|
328
|
+
distillLesson(context: {
|
|
329
|
+
situation: string;
|
|
330
|
+
errorPattern: string;
|
|
331
|
+
oversight: string;
|
|
332
|
+
fix: string;
|
|
333
|
+
preventativeRule: string;
|
|
334
|
+
severity?: string;
|
|
335
|
+
applicableScope: string;
|
|
336
|
+
inverseLesson?: string;
|
|
337
|
+
confidence?: number;
|
|
338
|
+
}): Promise<{
|
|
339
|
+
lessonId: string;
|
|
340
|
+
patternId: string;
|
|
341
|
+
severity: string;
|
|
342
|
+
preventativeRule: string;
|
|
343
|
+
ruleConfidence: number;
|
|
344
|
+
applicableScope: string;
|
|
345
|
+
wireFormat: string;
|
|
346
|
+
memoryId: string;
|
|
347
|
+
}>;
|
|
348
|
+
/**
|
|
349
|
+
* Query lessons from memory (RFC-0011 §4.1).
|
|
350
|
+
*/
|
|
351
|
+
queryLessons(query?: string, options?: {
|
|
352
|
+
limit?: number;
|
|
353
|
+
}): Promise<any[]>;
|
|
354
|
+
/**
|
|
355
|
+
* Update a memory entry's heritage_chain (RFC-0011 §8).
|
|
356
|
+
*/
|
|
357
|
+
insertHeritage(memoryId: string, heritage: {
|
|
358
|
+
intentChain: string[];
|
|
359
|
+
hypotheses: string[];
|
|
360
|
+
rationales: string[];
|
|
361
|
+
}): Promise<void>;
|
|
362
|
+
/**
|
|
363
|
+
* Return all memories whose lesson_pattern_id matches patternId (RFC-0011 §4.1).
|
|
364
|
+
*/
|
|
365
|
+
getMemoriesByPattern(patternId: string): Promise<any[]>;
|
|
366
|
+
/**
|
|
367
|
+
* S-MORA: Singularity Memory-Oriented Retrieval Augmentation (RFC-0012)
|
|
368
|
+
* 5-layer pipeline: Scrubbing → HyDE-Lite → Multi-channel retrieval → RRF → Heritage-aware reranking
|
|
369
|
+
*/
|
|
370
|
+
smora(query: string, options?: {
|
|
371
|
+
limit?: number;
|
|
372
|
+
retrievalLimit?: number;
|
|
373
|
+
sessionIntent?: string[];
|
|
374
|
+
enableSynthesis?: boolean;
|
|
375
|
+
enableHyDE?: boolean;
|
|
376
|
+
useCache?: boolean;
|
|
377
|
+
}): Promise<{
|
|
378
|
+
results: Array<{
|
|
379
|
+
id: string;
|
|
380
|
+
content: string;
|
|
381
|
+
metadata: Record<string, unknown>;
|
|
382
|
+
score: number;
|
|
383
|
+
semanticScore: number;
|
|
384
|
+
heritageBonus: number;
|
|
385
|
+
recencyDecay: number;
|
|
386
|
+
rrfRank: number;
|
|
387
|
+
}>;
|
|
388
|
+
synthesis?: string;
|
|
389
|
+
pipeline: {
|
|
390
|
+
queryExpanded: boolean;
|
|
391
|
+
heritageAware: boolean;
|
|
392
|
+
synthesized: boolean;
|
|
393
|
+
latencyMs: number;
|
|
394
|
+
};
|
|
395
|
+
}>;
|
|
287
396
|
getAll(options?: {}): Promise<any>;
|
|
288
397
|
stats(): Promise<{
|
|
289
398
|
count: number;
|
|
@@ -1063,7 +1063,7 @@ description: Auto-generated skill to handle: ${enrichedPrompt || topic}
|
|
|
1063
1063
|
* Note: YAMO emission is non-critical - failures are logged but don't throw
|
|
1064
1064
|
* to prevent disrupting the main operation.
|
|
1065
1065
|
*/
|
|
1066
|
-
async _emitYamoBlock(operationType, memoryId, yamoText) {
|
|
1066
|
+
async _emitYamoBlock(operationType, memoryId, yamoText, heritage) {
|
|
1067
1067
|
if (!this.yamoTable) {
|
|
1068
1068
|
return;
|
|
1069
1069
|
}
|
|
@@ -1081,6 +1081,7 @@ description: Auto-generated skill to handle: ${enrichedPrompt || topic}
|
|
|
1081
1081
|
metadata: JSON.stringify({
|
|
1082
1082
|
memory_id: memoryId || null,
|
|
1083
1083
|
timestamp: new Date().toISOString(),
|
|
1084
|
+
...(heritage ? { heritage_chain: heritage } : {}),
|
|
1084
1085
|
}),
|
|
1085
1086
|
},
|
|
1086
1087
|
]);
|
|
@@ -1310,6 +1311,310 @@ description: Auto-generated skill to handle: ${enrichedPrompt || topic}
|
|
|
1310
1311
|
}
|
|
1311
1312
|
: null;
|
|
1312
1313
|
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Delete a memory entry by ID.
|
|
1316
|
+
*/
|
|
1317
|
+
async delete(id) {
|
|
1318
|
+
await this.init();
|
|
1319
|
+
if (!this.client) {
|
|
1320
|
+
throw new Error("Database client not initialized");
|
|
1321
|
+
}
|
|
1322
|
+
try {
|
|
1323
|
+
await this.client.delete(id);
|
|
1324
|
+
this.keywordSearch?.remove?.(id);
|
|
1325
|
+
}
|
|
1326
|
+
catch (error) {
|
|
1327
|
+
if (error instanceof Error && error.message.includes("not found"))
|
|
1328
|
+
return;
|
|
1329
|
+
throw error;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Distill a LessonLearned block (RFC-0011 §3.5).
|
|
1334
|
+
* Idempotent: same patternId + equal/higher confidence returns existing.
|
|
1335
|
+
*/
|
|
1336
|
+
async distillLesson(context) {
|
|
1337
|
+
await this.init();
|
|
1338
|
+
const { situation, errorPattern, oversight, fix, preventativeRule, severity = "medium", applicableScope, inverseLesson = "", confidence = 0.7, } = context;
|
|
1339
|
+
const patternId = crypto.createHash("sha256")
|
|
1340
|
+
.update(errorPattern + applicableScope).digest("hex").slice(0, 16);
|
|
1341
|
+
// Idempotency check
|
|
1342
|
+
const existing = await this.getMemoriesByPattern(patternId);
|
|
1343
|
+
if (existing.length > 0) {
|
|
1344
|
+
const meta = typeof existing[0].metadata === "string"
|
|
1345
|
+
? JSON.parse(existing[0].metadata) : existing[0].metadata;
|
|
1346
|
+
if ((meta.rule_confidence ?? 0) >= confidence) {
|
|
1347
|
+
return {
|
|
1348
|
+
lessonId: meta.lesson_id, patternId, severity: meta.severity || severity,
|
|
1349
|
+
preventativeRule: meta.preventative_rule || preventativeRule,
|
|
1350
|
+
ruleConfidence: meta.rule_confidence, applicableScope: meta.applicable_scope || applicableScope,
|
|
1351
|
+
wireFormat: meta.yamo_wire_format || "", memoryId: existing[0].id,
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
const lessonId = `lesson_${Date.now()}_${crypto.randomBytes(3).toString("hex")}`;
|
|
1356
|
+
const timestamp = new Date().toISOString();
|
|
1357
|
+
const wireFormat = [
|
|
1358
|
+
`agent: MemoryMesh_${this.agentId};`,
|
|
1359
|
+
`intent: distill_wisdom_from_execution;`,
|
|
1360
|
+
`context:`,
|
|
1361
|
+
` original_context;${situation.replace(/;/g, ",")};`,
|
|
1362
|
+
` error_pattern;${patternId};`,
|
|
1363
|
+
` severity;${severity};`,
|
|
1364
|
+
` timestamp;${timestamp};`,
|
|
1365
|
+
`constraints:`,
|
|
1366
|
+
` hypothesis;This lesson prevents recurrence of similar failures;`,
|
|
1367
|
+
` hypothesis_confidence;${confidence};`,
|
|
1368
|
+
`priority: high;`,
|
|
1369
|
+
`output:`,
|
|
1370
|
+
` lesson_id;${lessonId};`,
|
|
1371
|
+
` oversight_description;${oversight.replace(/;/g, ",")};`,
|
|
1372
|
+
` preventative_rule;${preventativeRule.replace(/;/g, ",")};`,
|
|
1373
|
+
` rule_confidence;${confidence};`,
|
|
1374
|
+
`meta:`,
|
|
1375
|
+
` rationale;${fix.replace(/;/g, ",")};`,
|
|
1376
|
+
` applicability_scope;${applicableScope.replace(/;/g, ",")};`,
|
|
1377
|
+
` inverse_lesson;${inverseLesson.replace(/;/g, ",")};`,
|
|
1378
|
+
` confidence;${confidence};`,
|
|
1379
|
+
`log: lesson_learned;timestamp;${timestamp};pattern;${patternId};severity;${severity};id;${lessonId};`,
|
|
1380
|
+
`handoff: SubconsciousReflector;`,
|
|
1381
|
+
].join("\n");
|
|
1382
|
+
const lessonContent = `[LESSON:${patternId}] ${oversight} | Rule: ${preventativeRule} | Scope: ${applicableScope}`;
|
|
1383
|
+
const lessonMetadata = {
|
|
1384
|
+
type: "lesson", tags: ["#lesson_learned"], lesson_id: lessonId,
|
|
1385
|
+
lesson_pattern_id: patternId, severity, oversight, preventative_rule: preventativeRule,
|
|
1386
|
+
rule_confidence: confidence, applicable_scope: applicableScope, inverse_lesson: inverseLesson,
|
|
1387
|
+
yamo_wire_format: wireFormat, source: "distillLesson",
|
|
1388
|
+
};
|
|
1389
|
+
const mem = await this.add(lessonContent, lessonMetadata);
|
|
1390
|
+
if (this.enableYamo) {
|
|
1391
|
+
this._emitYamoBlock("lesson", mem.id, wireFormat).catch(() => { });
|
|
1392
|
+
}
|
|
1393
|
+
return { lessonId, patternId, severity, preventativeRule, ruleConfidence: confidence, applicableScope, wireFormat, memoryId: mem.id };
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Query lessons from memory (RFC-0011 §4.1).
|
|
1397
|
+
*/
|
|
1398
|
+
async queryLessons(query = "", options = {}) {
|
|
1399
|
+
await this.init();
|
|
1400
|
+
const limit = options.limit || 10;
|
|
1401
|
+
const all = await this.getAll({ limit: 1000 });
|
|
1402
|
+
const lessons = all.filter((r) => {
|
|
1403
|
+
try {
|
|
1404
|
+
const meta = typeof r.metadata === "string" ? JSON.parse(r.metadata) : r.metadata;
|
|
1405
|
+
return meta.type === "lesson" || (Array.isArray(meta.tags) && meta.tags.includes("#lesson_learned"));
|
|
1406
|
+
}
|
|
1407
|
+
catch {
|
|
1408
|
+
return false;
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1411
|
+
let scored = lessons;
|
|
1412
|
+
if (query) {
|
|
1413
|
+
const q = query.toLowerCase();
|
|
1414
|
+
scored = lessons.map((r) => ({
|
|
1415
|
+
...r,
|
|
1416
|
+
_score: (r.content?.toLowerCase().includes(q) ? 2 : 0) +
|
|
1417
|
+
(JSON.stringify(r.metadata).toLowerCase().includes(q) ? 1 : 0),
|
|
1418
|
+
})).sort((a, b) => b._score - a._score);
|
|
1419
|
+
}
|
|
1420
|
+
return scored.slice(0, limit).map((r) => {
|
|
1421
|
+
const meta = typeof r.metadata === "string" ? JSON.parse(r.metadata) : r.metadata;
|
|
1422
|
+
return {
|
|
1423
|
+
lessonId: meta.lesson_id || r.id, patternId: meta.lesson_pattern_id || "",
|
|
1424
|
+
severity: meta.severity || "medium", preventativeRule: meta.preventative_rule || "",
|
|
1425
|
+
ruleConfidence: meta.rule_confidence ?? 0, applicableScope: meta.applicable_scope || "",
|
|
1426
|
+
wireFormat: meta.yamo_wire_format || "", memoryId: r.id,
|
|
1427
|
+
};
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
/**
|
|
1431
|
+
* Update a memory entry's heritage_chain (RFC-0011 §8).
|
|
1432
|
+
*/
|
|
1433
|
+
async insertHeritage(memoryId, heritage) {
|
|
1434
|
+
await this.init();
|
|
1435
|
+
if (!this.client)
|
|
1436
|
+
throw new Error("Database client not initialized");
|
|
1437
|
+
try {
|
|
1438
|
+
const record = await this.client.getById(memoryId);
|
|
1439
|
+
if (!record)
|
|
1440
|
+
return;
|
|
1441
|
+
const existingMeta = typeof record.metadata === "string"
|
|
1442
|
+
? JSON.parse(record.metadata) : (record.metadata || {});
|
|
1443
|
+
await this.client.update(memoryId, {
|
|
1444
|
+
metadata: JSON.stringify({ ...existingMeta, heritage_chain: JSON.stringify(heritage) }),
|
|
1445
|
+
});
|
|
1446
|
+
// Emit RFC-0007 §5 heritage block
|
|
1447
|
+
if (this.enableYamo) {
|
|
1448
|
+
const ts = new Date().toISOString();
|
|
1449
|
+
const heritageBlock = [
|
|
1450
|
+
`agent: MemoryMesh_${this.agentId};`,
|
|
1451
|
+
`intent: record_heritage_chain;`,
|
|
1452
|
+
`context:`,
|
|
1453
|
+
` memory_id;${memoryId};`,
|
|
1454
|
+
` intent_chain;${heritage.intentChain.join(",")};`,
|
|
1455
|
+
` timestamp;${ts};`,
|
|
1456
|
+
`output:`,
|
|
1457
|
+
` heritage_recorded;true;`,
|
|
1458
|
+
`log: heritage_inserted;memory;${memoryId};timestamp;${ts};`,
|
|
1459
|
+
`handoff: End;`,
|
|
1460
|
+
].join("\n");
|
|
1461
|
+
this._emitYamoBlock("heritage", memoryId, heritageBlock, heritage).catch(() => { });
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
catch (error) {
|
|
1465
|
+
if (error instanceof Error && error.message.includes("not found"))
|
|
1466
|
+
return;
|
|
1467
|
+
throw error;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Return all memories whose lesson_pattern_id matches patternId (RFC-0011 §4.1).
|
|
1472
|
+
*/
|
|
1473
|
+
async getMemoriesByPattern(patternId) {
|
|
1474
|
+
await this.init();
|
|
1475
|
+
const all = await this.getAll({ limit: 1000 });
|
|
1476
|
+
return all.filter((r) => {
|
|
1477
|
+
try {
|
|
1478
|
+
const meta = typeof r.metadata === "string" ? JSON.parse(r.metadata) : r.metadata;
|
|
1479
|
+
return meta.lesson_pattern_id === patternId;
|
|
1480
|
+
}
|
|
1481
|
+
catch {
|
|
1482
|
+
return false;
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* S-MORA: Singularity Memory-Oriented Retrieval Augmentation (RFC-0012)
|
|
1488
|
+
* 5-layer pipeline: Scrubbing → HyDE-Lite → Multi-channel retrieval → RRF → Heritage-aware reranking
|
|
1489
|
+
*/
|
|
1490
|
+
async smora(query, options = {}) {
|
|
1491
|
+
await this.init();
|
|
1492
|
+
const t0 = Date.now();
|
|
1493
|
+
const { limit = 10, retrievalLimit = 30, sessionIntent = [], enableSynthesis = false, enableHyDE = true, } = options;
|
|
1494
|
+
// Layer 0: scrub query
|
|
1495
|
+
let scrubbed = query;
|
|
1496
|
+
try {
|
|
1497
|
+
if (this.scrubber) {
|
|
1498
|
+
const s = await this.scrubber.process({ content: query });
|
|
1499
|
+
scrubbed = s.content || query;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
catch { /* non-fatal */ }
|
|
1503
|
+
if (!this.client) {
|
|
1504
|
+
return { results: [], pipeline: { queryExpanded: false, heritageAware: false, synthesized: false, latencyMs: Date.now() - t0 } };
|
|
1505
|
+
}
|
|
1506
|
+
// Layer 1: HyDE-Lite — template-based query expansion (no LLM required)
|
|
1507
|
+
let hydeQuery = null;
|
|
1508
|
+
if (enableHyDE) {
|
|
1509
|
+
hydeQuery = `A document about ${scrubbed}. This covers concepts related to ${scrubbed} including patterns, insights, and lessons learned.`;
|
|
1510
|
+
}
|
|
1511
|
+
// Layer 2: Multi-channel retrieval (semantic original, semantic HyDE, keyword BM25)
|
|
1512
|
+
const queryVec = await this.embeddingFactory.embed(scrubbed);
|
|
1513
|
+
const hydeVec = hydeQuery ? await this.embeddingFactory.embed(hydeQuery) : null;
|
|
1514
|
+
const [semanticOrig, semanticHyde, keywordResults] = await Promise.all([
|
|
1515
|
+
this.client.search(queryVec, { limit: retrievalLimit, metric: 'cosine' }),
|
|
1516
|
+
hydeVec ? this.client.search(hydeVec, { limit: retrievalLimit, metric: 'cosine' }) : Promise.resolve([]),
|
|
1517
|
+
Promise.resolve(this.keywordSearch.search(scrubbed, { limit: retrievalLimit })),
|
|
1518
|
+
]);
|
|
1519
|
+
// Layer 3: Reciprocal Rank Fusion (k=60, channel weights: 1.0 / 0.8 / 0.6)
|
|
1520
|
+
const k = 60;
|
|
1521
|
+
const weights = { orig: 1.0, hyde: 0.8, keyword: 0.6 };
|
|
1522
|
+
const rrfScores = new Map();
|
|
1523
|
+
const docMap = new Map();
|
|
1524
|
+
function applyRRF(list, weight) {
|
|
1525
|
+
for (let rank = 0; rank < list.length; rank++) {
|
|
1526
|
+
const doc = list[rank];
|
|
1527
|
+
if (!doc?.id)
|
|
1528
|
+
continue;
|
|
1529
|
+
rrfScores.set(doc.id, (rrfScores.get(doc.id) || 0) + weight / (k + rank + 1));
|
|
1530
|
+
if (!docMap.has(doc.id))
|
|
1531
|
+
docMap.set(doc.id, doc);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
applyRRF(semanticOrig, weights.orig);
|
|
1535
|
+
applyRRF(semanticHyde, weights.hyde);
|
|
1536
|
+
applyRRF(keywordResults.map((r) => ({ id: r.id, content: r.content, metadata: r.metadata, created_at: r.created_at || new Date().toISOString() })), weights.keyword);
|
|
1537
|
+
// Take top pre_rerank_limit candidates
|
|
1538
|
+
const preRerankLimit = 20;
|
|
1539
|
+
const candidates = Array.from(rrfScores.entries())
|
|
1540
|
+
.sort((a, b) => b[1] - a[1])
|
|
1541
|
+
.slice(0, preRerankLimit)
|
|
1542
|
+
.map(([id]) => docMap.get(id))
|
|
1543
|
+
.filter(Boolean);
|
|
1544
|
+
// Layer 4: Heritage-aware reranking
|
|
1545
|
+
// final_score = 0.6×semantic_sim + 0.25×heritage_bonus + 0.15×recency_decay
|
|
1546
|
+
// When no sessionIntent: weights renormalize → α=0.71, γ=0.29
|
|
1547
|
+
const hasHeritage = sessionIntent.length > 0;
|
|
1548
|
+
const α = hasHeritage ? 0.6 : 0.71;
|
|
1549
|
+
const β = hasHeritage ? 0.25 : 0.0;
|
|
1550
|
+
const γ = hasHeritage ? 0.15 : 0.29;
|
|
1551
|
+
const λ = 0.05; // recency decay rate (half-life ≈ 14 days)
|
|
1552
|
+
const now = Date.now();
|
|
1553
|
+
const reranked = candidates.map((doc) => {
|
|
1554
|
+
// Semantic score: normalize rrfScore to [0,1] approximation
|
|
1555
|
+
const rrfScore = rrfScores.get(doc.id) || 0;
|
|
1556
|
+
const semanticScore = Math.min(1.0, rrfScore * 20); // scale to ~[0,1]
|
|
1557
|
+
// Heritage bonus
|
|
1558
|
+
let heritageBonus = 0;
|
|
1559
|
+
if (hasHeritage) {
|
|
1560
|
+
try {
|
|
1561
|
+
const meta = typeof doc.metadata === 'string' ? JSON.parse(doc.metadata) : doc.metadata;
|
|
1562
|
+
const chain = meta?.heritage_chain;
|
|
1563
|
+
const parsedChain = typeof chain === 'string' ? JSON.parse(chain) : chain;
|
|
1564
|
+
const intentChain = parsedChain?.intentChain ?? [];
|
|
1565
|
+
const overlap = intentChain.filter((i) => sessionIntent.includes(i)).length;
|
|
1566
|
+
if (overlap > 0)
|
|
1567
|
+
heritageBonus = Math.min(1.0, overlap / sessionIntent.length);
|
|
1568
|
+
}
|
|
1569
|
+
catch { /* no heritage */ }
|
|
1570
|
+
}
|
|
1571
|
+
// Recency decay: exp(-λ × age_days)
|
|
1572
|
+
let recencyDecay = 1.0;
|
|
1573
|
+
try {
|
|
1574
|
+
const createdAt = doc.created_at || doc.metadata?.created_at;
|
|
1575
|
+
if (createdAt) {
|
|
1576
|
+
const ageDays = (now - new Date(createdAt).getTime()) / 86400000;
|
|
1577
|
+
recencyDecay = Math.exp(-λ * ageDays);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
catch { /* use 1.0 */ }
|
|
1581
|
+
const score = α * semanticScore + β * heritageBonus + γ * recencyDecay;
|
|
1582
|
+
const meta = typeof doc.metadata === 'string' ? JSON.parse(doc.metadata) : doc.metadata;
|
|
1583
|
+
return { doc, score, semanticScore, heritageBonus, recencyDecay, meta };
|
|
1584
|
+
});
|
|
1585
|
+
reranked.sort((a, b) => b.score - a.score);
|
|
1586
|
+
const results = reranked.slice(0, limit).map(({ doc, score, semanticScore, heritageBonus, recencyDecay, meta }, idx) => ({
|
|
1587
|
+
id: doc.id,
|
|
1588
|
+
content: doc.content,
|
|
1589
|
+
metadata: meta,
|
|
1590
|
+
score,
|
|
1591
|
+
semanticScore,
|
|
1592
|
+
heritageBonus,
|
|
1593
|
+
recencyDecay,
|
|
1594
|
+
rrfRank: idx + 1,
|
|
1595
|
+
}));
|
|
1596
|
+
// Layer 5: Synthesis (skip if LLM unavailable)
|
|
1597
|
+
let synthesis;
|
|
1598
|
+
let synthesized = false;
|
|
1599
|
+
if (enableSynthesis && this.llmClient) {
|
|
1600
|
+
try {
|
|
1601
|
+
const excerpts = results.slice(0, 5).map((r, i) => `[${i + 1}] ${r.content}`).join('\n');
|
|
1602
|
+
synthesis = await this.llmClient.complete(`You are a retrieval synthesis agent. Given the following memory excerpts, produce a coherent summary that directly answers the query.\nQuery: ${scrubbed}\nExcerpts:\n${excerpts}`);
|
|
1603
|
+
synthesized = true;
|
|
1604
|
+
}
|
|
1605
|
+
catch { /* non-fatal, skip synthesis */ }
|
|
1606
|
+
}
|
|
1607
|
+
return {
|
|
1608
|
+
results,
|
|
1609
|
+
...(synthesis !== undefined ? { synthesis } : {}),
|
|
1610
|
+
pipeline: {
|
|
1611
|
+
queryExpanded: enableHyDE,
|
|
1612
|
+
heritageAware: hasHeritage,
|
|
1613
|
+
synthesized,
|
|
1614
|
+
latencyMs: Date.now() - t0,
|
|
1615
|
+
},
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1313
1618
|
async getAll(options = {}) {
|
|
1314
1619
|
await this.init();
|
|
1315
1620
|
if (!this.client) {
|
|
@@ -30,6 +30,38 @@ import { LLMClient } from "../llm/client.js";
|
|
|
30
30
|
import * as lancedb from "@lancedb/lancedb";
|
|
31
31
|
import { createLogger } from "../utils/logger.js";
|
|
32
32
|
const logger = createLogger("brain");
|
|
33
|
+
/** RFC-0012 S-MORA types */
|
|
34
|
+
export interface SMORAOptions {
|
|
35
|
+
limit?: number;
|
|
36
|
+
retrievalLimit?: number;
|
|
37
|
+
sessionIntent?: string[];
|
|
38
|
+
enableSynthesis?: boolean;
|
|
39
|
+
enableHyDE?: boolean;
|
|
40
|
+
useCache?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SMORAResult {
|
|
44
|
+
id: string;
|
|
45
|
+
content: string;
|
|
46
|
+
metadata: Record<string, unknown>;
|
|
47
|
+
score: number;
|
|
48
|
+
semanticScore: number;
|
|
49
|
+
heritageBonus: number;
|
|
50
|
+
recencyDecay: number;
|
|
51
|
+
rrfRank: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface SMORAResponse {
|
|
55
|
+
results: SMORAResult[];
|
|
56
|
+
synthesis?: string;
|
|
57
|
+
pipeline: {
|
|
58
|
+
queryExpanded: boolean;
|
|
59
|
+
heritageAware: boolean;
|
|
60
|
+
synthesized: boolean;
|
|
61
|
+
latencyMs: number;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
33
65
|
/**
|
|
34
66
|
* MemoryMesh class for managing vector memory storage
|
|
35
67
|
*/
|
|
@@ -1063,7 +1095,7 @@ description: Auto-generated skill to handle: ${enrichedPrompt || topic}
|
|
|
1063
1095
|
* Note: YAMO emission is non-critical - failures are logged but don't throw
|
|
1064
1096
|
* to prevent disrupting the main operation.
|
|
1065
1097
|
*/
|
|
1066
|
-
async _emitYamoBlock(operationType, memoryId, yamoText) {
|
|
1098
|
+
async _emitYamoBlock(operationType, memoryId, yamoText, heritage?: { intentChain: string[]; hypotheses: string[]; rationales: string[] }) {
|
|
1067
1099
|
if (!this.yamoTable) {
|
|
1068
1100
|
return;
|
|
1069
1101
|
}
|
|
@@ -1081,6 +1113,7 @@ description: Auto-generated skill to handle: ${enrichedPrompt || topic}
|
|
|
1081
1113
|
metadata: JSON.stringify({
|
|
1082
1114
|
memory_id: memoryId || null,
|
|
1083
1115
|
timestamp: new Date().toISOString(),
|
|
1116
|
+
...(heritage ? { heritage_chain: heritage } : {}),
|
|
1084
1117
|
}),
|
|
1085
1118
|
},
|
|
1086
1119
|
]);
|
|
@@ -1310,6 +1343,365 @@ description: Auto-generated skill to handle: ${enrichedPrompt || topic}
|
|
|
1310
1343
|
}
|
|
1311
1344
|
: null;
|
|
1312
1345
|
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Delete a memory entry by ID.
|
|
1348
|
+
*/
|
|
1349
|
+
async delete(id: string): Promise<void> {
|
|
1350
|
+
await this.init();
|
|
1351
|
+
if (!this.client) {
|
|
1352
|
+
throw new Error("Database client not initialized");
|
|
1353
|
+
}
|
|
1354
|
+
try {
|
|
1355
|
+
await this.client.delete(id);
|
|
1356
|
+
this.keywordSearch?.remove?.(id);
|
|
1357
|
+
} catch (error: any) {
|
|
1358
|
+
if (error instanceof Error && error.message.includes("not found")) return;
|
|
1359
|
+
throw error;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Distill a LessonLearned block (RFC-0011 §3.5).
|
|
1364
|
+
* Idempotent: same patternId + equal/higher confidence returns existing.
|
|
1365
|
+
*/
|
|
1366
|
+
async distillLesson(context: {
|
|
1367
|
+
situation: string;
|
|
1368
|
+
errorPattern: string;
|
|
1369
|
+
oversight: string;
|
|
1370
|
+
fix: string;
|
|
1371
|
+
preventativeRule: string;
|
|
1372
|
+
severity?: string;
|
|
1373
|
+
applicableScope: string;
|
|
1374
|
+
inverseLesson?: string;
|
|
1375
|
+
confidence?: number;
|
|
1376
|
+
}): Promise<{
|
|
1377
|
+
lessonId: string;
|
|
1378
|
+
patternId: string;
|
|
1379
|
+
severity: string;
|
|
1380
|
+
preventativeRule: string;
|
|
1381
|
+
ruleConfidence: number;
|
|
1382
|
+
applicableScope: string;
|
|
1383
|
+
wireFormat: string;
|
|
1384
|
+
memoryId: string;
|
|
1385
|
+
}> {
|
|
1386
|
+
await this.init();
|
|
1387
|
+
const {
|
|
1388
|
+
situation, errorPattern, oversight, fix, preventativeRule,
|
|
1389
|
+
severity = "medium", applicableScope, inverseLesson = "", confidence = 0.7,
|
|
1390
|
+
} = context;
|
|
1391
|
+
const patternId = crypto.createHash("sha256")
|
|
1392
|
+
.update(errorPattern + applicableScope).digest("hex").slice(0, 16);
|
|
1393
|
+
// Idempotency check
|
|
1394
|
+
const existing = await this.getMemoriesByPattern(patternId);
|
|
1395
|
+
if (existing.length > 0) {
|
|
1396
|
+
const meta = typeof existing[0].metadata === "string"
|
|
1397
|
+
? JSON.parse(existing[0].metadata) : existing[0].metadata;
|
|
1398
|
+
if ((meta.rule_confidence ?? 0) >= confidence) {
|
|
1399
|
+
return {
|
|
1400
|
+
lessonId: meta.lesson_id, patternId, severity: meta.severity || severity,
|
|
1401
|
+
preventativeRule: meta.preventative_rule || preventativeRule,
|
|
1402
|
+
ruleConfidence: meta.rule_confidence, applicableScope: meta.applicable_scope || applicableScope,
|
|
1403
|
+
wireFormat: meta.yamo_wire_format || "", memoryId: existing[0].id,
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
const lessonId = `lesson_${Date.now()}_${crypto.randomBytes(3).toString("hex")}`;
|
|
1408
|
+
const timestamp = new Date().toISOString();
|
|
1409
|
+
const wireFormat = [
|
|
1410
|
+
`agent: MemoryMesh_${this.agentId};`,
|
|
1411
|
+
`intent: distill_wisdom_from_execution;`,
|
|
1412
|
+
`context:`,
|
|
1413
|
+
` original_context;${situation.replace(/;/g, ",")};`,
|
|
1414
|
+
` error_pattern;${patternId};`,
|
|
1415
|
+
` severity;${severity};`,
|
|
1416
|
+
` timestamp;${timestamp};`,
|
|
1417
|
+
`constraints:`,
|
|
1418
|
+
` hypothesis;This lesson prevents recurrence of similar failures;`,
|
|
1419
|
+
` hypothesis_confidence;${confidence};`,
|
|
1420
|
+
`priority: high;`,
|
|
1421
|
+
`output:`,
|
|
1422
|
+
` lesson_id;${lessonId};`,
|
|
1423
|
+
` oversight_description;${oversight.replace(/;/g, ",")};`,
|
|
1424
|
+
` preventative_rule;${preventativeRule.replace(/;/g, ",")};`,
|
|
1425
|
+
` rule_confidence;${confidence};`,
|
|
1426
|
+
`meta:`,
|
|
1427
|
+
` rationale;${fix.replace(/;/g, ",")};`,
|
|
1428
|
+
` applicability_scope;${applicableScope.replace(/;/g, ",")};`,
|
|
1429
|
+
` inverse_lesson;${inverseLesson.replace(/;/g, ",")};`,
|
|
1430
|
+
` confidence;${confidence};`,
|
|
1431
|
+
`log: lesson_learned;timestamp;${timestamp};pattern;${patternId};severity;${severity};id;${lessonId};`,
|
|
1432
|
+
`handoff: SubconsciousReflector;`,
|
|
1433
|
+
].join("\n");
|
|
1434
|
+
const lessonContent = `[LESSON:${patternId}] ${oversight} | Rule: ${preventativeRule} | Scope: ${applicableScope}`;
|
|
1435
|
+
const lessonMetadata = {
|
|
1436
|
+
type: "lesson", tags: ["#lesson_learned"], lesson_id: lessonId,
|
|
1437
|
+
lesson_pattern_id: patternId, severity, oversight, preventative_rule: preventativeRule,
|
|
1438
|
+
rule_confidence: confidence, applicable_scope: applicableScope, inverse_lesson: inverseLesson,
|
|
1439
|
+
yamo_wire_format: wireFormat, source: "distillLesson",
|
|
1440
|
+
};
|
|
1441
|
+
const mem = await this.add(lessonContent, lessonMetadata);
|
|
1442
|
+
if (this.enableYamo) {
|
|
1443
|
+
this._emitYamoBlock("lesson", mem.id, wireFormat).catch(() => {});
|
|
1444
|
+
}
|
|
1445
|
+
return { lessonId, patternId, severity, preventativeRule, ruleConfidence: confidence, applicableScope, wireFormat, memoryId: mem.id };
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Query lessons from memory (RFC-0011 §4.1).
|
|
1449
|
+
*/
|
|
1450
|
+
async queryLessons(query = "", options: { limit?: number } = {}): Promise<any[]> {
|
|
1451
|
+
await this.init();
|
|
1452
|
+
const limit = options.limit || 10;
|
|
1453
|
+
const all = await this.getAll({ limit: 1000 });
|
|
1454
|
+
const lessons = all.filter((r: any) => {
|
|
1455
|
+
try {
|
|
1456
|
+
const meta = typeof r.metadata === "string" ? JSON.parse(r.metadata) : r.metadata;
|
|
1457
|
+
return meta.type === "lesson" || (Array.isArray(meta.tags) && meta.tags.includes("#lesson_learned"));
|
|
1458
|
+
} catch { return false; }
|
|
1459
|
+
});
|
|
1460
|
+
let scored = lessons as any[];
|
|
1461
|
+
if (query) {
|
|
1462
|
+
const q = query.toLowerCase();
|
|
1463
|
+
scored = lessons.map((r: any) => ({
|
|
1464
|
+
...r,
|
|
1465
|
+
_score: (r.content?.toLowerCase().includes(q) ? 2 : 0) +
|
|
1466
|
+
(JSON.stringify(r.metadata).toLowerCase().includes(q) ? 1 : 0),
|
|
1467
|
+
})).sort((a: any, b: any) => b._score - a._score);
|
|
1468
|
+
}
|
|
1469
|
+
return scored.slice(0, limit).map((r: any) => {
|
|
1470
|
+
const meta = typeof r.metadata === "string" ? JSON.parse(r.metadata) : r.metadata;
|
|
1471
|
+
return {
|
|
1472
|
+
lessonId: meta.lesson_id || r.id, patternId: meta.lesson_pattern_id || "",
|
|
1473
|
+
severity: meta.severity || "medium", preventativeRule: meta.preventative_rule || "",
|
|
1474
|
+
ruleConfidence: meta.rule_confidence ?? 0, applicableScope: meta.applicable_scope || "",
|
|
1475
|
+
wireFormat: meta.yamo_wire_format || "", memoryId: r.id,
|
|
1476
|
+
};
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Update a memory entry's heritage_chain (RFC-0011 §8).
|
|
1481
|
+
*/
|
|
1482
|
+
async insertHeritage(memoryId: string, heritage: { intentChain: string[]; hypotheses: string[]; rationales: string[] }): Promise<void> {
|
|
1483
|
+
await this.init();
|
|
1484
|
+
if (!this.client) throw new Error("Database client not initialized");
|
|
1485
|
+
try {
|
|
1486
|
+
const record = await this.client.getById(memoryId);
|
|
1487
|
+
if (!record) return;
|
|
1488
|
+
const existingMeta = typeof record.metadata === "string"
|
|
1489
|
+
? JSON.parse(record.metadata) : (record.metadata || {});
|
|
1490
|
+
await this.client.update(memoryId, {
|
|
1491
|
+
metadata: JSON.stringify({ ...existingMeta, heritage_chain: JSON.stringify(heritage) }),
|
|
1492
|
+
});
|
|
1493
|
+
// Emit RFC-0007 §5 heritage block
|
|
1494
|
+
if (this.enableYamo) {
|
|
1495
|
+
const ts = new Date().toISOString();
|
|
1496
|
+
const heritageBlock = [
|
|
1497
|
+
`agent: MemoryMesh_${this.agentId};`,
|
|
1498
|
+
`intent: record_heritage_chain;`,
|
|
1499
|
+
`context:`,
|
|
1500
|
+
` memory_id;${memoryId};`,
|
|
1501
|
+
` intent_chain;${heritage.intentChain.join(",")};`,
|
|
1502
|
+
` timestamp;${ts};`,
|
|
1503
|
+
`output:`,
|
|
1504
|
+
` heritage_recorded;true;`,
|
|
1505
|
+
`log: heritage_inserted;memory;${memoryId};timestamp;${ts};`,
|
|
1506
|
+
`handoff: End;`,
|
|
1507
|
+
].join("\n");
|
|
1508
|
+
this._emitYamoBlock("heritage", memoryId, heritageBlock, heritage).catch(() => {});
|
|
1509
|
+
}
|
|
1510
|
+
} catch (error: any) {
|
|
1511
|
+
if (error instanceof Error && error.message.includes("not found")) return;
|
|
1512
|
+
throw error;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Return all memories whose lesson_pattern_id matches patternId (RFC-0011 §4.1).
|
|
1517
|
+
*/
|
|
1518
|
+
async getMemoriesByPattern(patternId: string): Promise<any[]> {
|
|
1519
|
+
await this.init();
|
|
1520
|
+
const all = await this.getAll({ limit: 1000 });
|
|
1521
|
+
return (all as any[]).filter((r) => {
|
|
1522
|
+
try {
|
|
1523
|
+
const meta = typeof r.metadata === "string" ? JSON.parse(r.metadata) : r.metadata;
|
|
1524
|
+
return meta.lesson_pattern_id === patternId;
|
|
1525
|
+
} catch { return false; }
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* S-MORA: Singularity Memory-Oriented Retrieval Augmentation (RFC-0012)
|
|
1531
|
+
* 5-layer pipeline: Scrubbing → HyDE-Lite → Multi-channel retrieval → RRF → Heritage-aware reranking
|
|
1532
|
+
*/
|
|
1533
|
+
async smora(query: string, options: {
|
|
1534
|
+
limit?: number;
|
|
1535
|
+
retrievalLimit?: number;
|
|
1536
|
+
sessionIntent?: string[];
|
|
1537
|
+
enableSynthesis?: boolean;
|
|
1538
|
+
enableHyDE?: boolean;
|
|
1539
|
+
useCache?: boolean;
|
|
1540
|
+
} = {}): Promise<{
|
|
1541
|
+
results: Array<{
|
|
1542
|
+
id: string;
|
|
1543
|
+
content: string;
|
|
1544
|
+
metadata: Record<string, unknown>;
|
|
1545
|
+
score: number;
|
|
1546
|
+
semanticScore: number;
|
|
1547
|
+
heritageBonus: number;
|
|
1548
|
+
recencyDecay: number;
|
|
1549
|
+
rrfRank: number;
|
|
1550
|
+
}>;
|
|
1551
|
+
synthesis?: string;
|
|
1552
|
+
pipeline: {
|
|
1553
|
+
queryExpanded: boolean;
|
|
1554
|
+
heritageAware: boolean;
|
|
1555
|
+
synthesized: boolean;
|
|
1556
|
+
latencyMs: number;
|
|
1557
|
+
};
|
|
1558
|
+
}> {
|
|
1559
|
+
await this.init();
|
|
1560
|
+
const t0 = Date.now();
|
|
1561
|
+
const {
|
|
1562
|
+
limit = 10,
|
|
1563
|
+
retrievalLimit = 30,
|
|
1564
|
+
sessionIntent = [],
|
|
1565
|
+
enableSynthesis = false,
|
|
1566
|
+
enableHyDE = true,
|
|
1567
|
+
} = options;
|
|
1568
|
+
|
|
1569
|
+
// Layer 0: scrub query
|
|
1570
|
+
let scrubbed = query;
|
|
1571
|
+
try {
|
|
1572
|
+
if (this.scrubber) {
|
|
1573
|
+
const s = await this.scrubber.process({ content: query });
|
|
1574
|
+
scrubbed = s.content || query;
|
|
1575
|
+
}
|
|
1576
|
+
} catch { /* non-fatal */ }
|
|
1577
|
+
|
|
1578
|
+
if (!this.client) {
|
|
1579
|
+
return { results: [], pipeline: { queryExpanded: false, heritageAware: false, synthesized: false, latencyMs: Date.now() - t0 } };
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// Layer 1: HyDE-Lite — template-based query expansion (no LLM required)
|
|
1583
|
+
let hydeQuery: string | null = null;
|
|
1584
|
+
if (enableHyDE) {
|
|
1585
|
+
hydeQuery = `A document about ${scrubbed}. This covers concepts related to ${scrubbed} including patterns, insights, and lessons learned.`;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// Layer 2: Multi-channel retrieval (semantic original, semantic HyDE, keyword BM25)
|
|
1589
|
+
const queryVec = await this.embeddingFactory.embed(scrubbed);
|
|
1590
|
+
const hydeVec = hydeQuery ? await this.embeddingFactory.embed(hydeQuery) : null;
|
|
1591
|
+
|
|
1592
|
+
const [semanticOrig, semanticHyde, keywordResults] = await Promise.all([
|
|
1593
|
+
this.client.search(queryVec, { limit: retrievalLimit, metric: 'cosine' }),
|
|
1594
|
+
hydeVec ? this.client.search(hydeVec, { limit: retrievalLimit, metric: 'cosine' }) : Promise.resolve([]),
|
|
1595
|
+
Promise.resolve(this.keywordSearch.search(scrubbed, { limit: retrievalLimit })),
|
|
1596
|
+
]);
|
|
1597
|
+
|
|
1598
|
+
// Layer 3: Reciprocal Rank Fusion (k=60, channel weights: 1.0 / 0.8 / 0.6)
|
|
1599
|
+
const k = 60;
|
|
1600
|
+
const weights = { orig: 1.0, hyde: 0.8, keyword: 0.6 };
|
|
1601
|
+
const rrfScores = new Map<string, number>();
|
|
1602
|
+
const docMap = new Map<string, any>();
|
|
1603
|
+
|
|
1604
|
+
function applyRRF(list: any[], weight: number) {
|
|
1605
|
+
for (let rank = 0; rank < list.length; rank++) {
|
|
1606
|
+
const doc = list[rank];
|
|
1607
|
+
if (!doc?.id) continue;
|
|
1608
|
+
rrfScores.set(doc.id, (rrfScores.get(doc.id) || 0) + weight / (k + rank + 1));
|
|
1609
|
+
if (!docMap.has(doc.id)) docMap.set(doc.id, doc);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
applyRRF(semanticOrig, weights.orig);
|
|
1613
|
+
applyRRF(semanticHyde, weights.hyde);
|
|
1614
|
+
applyRRF(keywordResults.map((r: any) => ({ id: r.id, content: r.content, metadata: r.metadata, created_at: r.created_at || new Date().toISOString() })), weights.keyword);
|
|
1615
|
+
|
|
1616
|
+
// Take top pre_rerank_limit candidates
|
|
1617
|
+
const preRerankLimit = 20;
|
|
1618
|
+
const candidates = Array.from(rrfScores.entries())
|
|
1619
|
+
.sort((a, b) => b[1] - a[1])
|
|
1620
|
+
.slice(0, preRerankLimit)
|
|
1621
|
+
.map(([id]) => docMap.get(id))
|
|
1622
|
+
.filter(Boolean);
|
|
1623
|
+
|
|
1624
|
+
// Layer 4: Heritage-aware reranking
|
|
1625
|
+
// final_score = 0.6×semantic_sim + 0.25×heritage_bonus + 0.15×recency_decay
|
|
1626
|
+
// When no sessionIntent: weights renormalize → α=0.71, γ=0.29
|
|
1627
|
+
const hasHeritage = sessionIntent.length > 0;
|
|
1628
|
+
const α = hasHeritage ? 0.6 : 0.71;
|
|
1629
|
+
const β = hasHeritage ? 0.25 : 0.0;
|
|
1630
|
+
const γ = hasHeritage ? 0.15 : 0.29;
|
|
1631
|
+
const λ = 0.05; // recency decay rate (half-life ≈ 14 days)
|
|
1632
|
+
const now = Date.now();
|
|
1633
|
+
|
|
1634
|
+
const reranked = candidates.map((doc: any) => {
|
|
1635
|
+
// Semantic score: normalize rrfScore to [0,1] approximation
|
|
1636
|
+
const rrfScore = rrfScores.get(doc.id) || 0;
|
|
1637
|
+
const semanticScore = Math.min(1.0, rrfScore * 20); // scale to ~[0,1]
|
|
1638
|
+
|
|
1639
|
+
// Heritage bonus
|
|
1640
|
+
let heritageBonus = 0;
|
|
1641
|
+
if (hasHeritage) {
|
|
1642
|
+
try {
|
|
1643
|
+
const meta = typeof doc.metadata === 'string' ? JSON.parse(doc.metadata) : doc.metadata;
|
|
1644
|
+
const chain = meta?.heritage_chain;
|
|
1645
|
+
const parsedChain = typeof chain === 'string' ? JSON.parse(chain) : chain;
|
|
1646
|
+
const intentChain: string[] = parsedChain?.intentChain ?? [];
|
|
1647
|
+
const overlap = intentChain.filter((i: string) => sessionIntent.includes(i)).length;
|
|
1648
|
+
if (overlap > 0) heritageBonus = Math.min(1.0, overlap / sessionIntent.length);
|
|
1649
|
+
} catch { /* no heritage */ }
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// Recency decay: exp(-λ × age_days)
|
|
1653
|
+
let recencyDecay = 1.0;
|
|
1654
|
+
try {
|
|
1655
|
+
const createdAt = doc.created_at || doc.metadata?.created_at;
|
|
1656
|
+
if (createdAt) {
|
|
1657
|
+
const ageDays = (now - new Date(createdAt).getTime()) / 86400000;
|
|
1658
|
+
recencyDecay = Math.exp(-λ * ageDays);
|
|
1659
|
+
}
|
|
1660
|
+
} catch { /* use 1.0 */ }
|
|
1661
|
+
|
|
1662
|
+
const score = α * semanticScore + β * heritageBonus + γ * recencyDecay;
|
|
1663
|
+
const meta = typeof doc.metadata === 'string' ? JSON.parse(doc.metadata) : doc.metadata;
|
|
1664
|
+
return { doc, score, semanticScore, heritageBonus, recencyDecay, meta };
|
|
1665
|
+
});
|
|
1666
|
+
|
|
1667
|
+
reranked.sort((a, b) => b.score - a.score);
|
|
1668
|
+
|
|
1669
|
+
const results = reranked.slice(0, limit).map(({ doc, score, semanticScore, heritageBonus, recencyDecay, meta }, idx) => ({
|
|
1670
|
+
id: doc.id,
|
|
1671
|
+
content: doc.content,
|
|
1672
|
+
metadata: meta,
|
|
1673
|
+
score,
|
|
1674
|
+
semanticScore,
|
|
1675
|
+
heritageBonus,
|
|
1676
|
+
recencyDecay,
|
|
1677
|
+
rrfRank: idx + 1,
|
|
1678
|
+
}));
|
|
1679
|
+
|
|
1680
|
+
// Layer 5: Synthesis (skip if LLM unavailable)
|
|
1681
|
+
let synthesis: string | undefined;
|
|
1682
|
+
let synthesized = false;
|
|
1683
|
+
if (enableSynthesis && this.llmClient) {
|
|
1684
|
+
try {
|
|
1685
|
+
const excerpts = results.slice(0, 5).map((r, i) => `[${i + 1}] ${r.content}`).join('\n');
|
|
1686
|
+
synthesis = await this.llmClient.complete(
|
|
1687
|
+
`You are a retrieval synthesis agent. Given the following memory excerpts, produce a coherent summary that directly answers the query.\nQuery: ${scrubbed}\nExcerpts:\n${excerpts}`
|
|
1688
|
+
);
|
|
1689
|
+
synthesized = true;
|
|
1690
|
+
} catch { /* non-fatal, skip synthesis */ }
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
return {
|
|
1694
|
+
results,
|
|
1695
|
+
...(synthesis !== undefined ? { synthesis } : {}),
|
|
1696
|
+
pipeline: {
|
|
1697
|
+
queryExpanded: enableHyDE,
|
|
1698
|
+
heritageAware: hasHeritage,
|
|
1699
|
+
synthesized,
|
|
1700
|
+
latencyMs: Date.now() - t0,
|
|
1701
|
+
},
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1313
1705
|
async getAll(options = {}) {
|
|
1314
1706
|
await this.init();
|
|
1315
1707
|
if (!this.client) {
|
package/package.json
CHANGED