metame-cli 1.5.3 → 1.5.5
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 +60 -18
- package/index.js +352 -79
- package/package.json +2 -2
- package/scripts/agent-layer.js +4 -2
- package/scripts/bin/dispatch_to +178 -90
- package/scripts/daemon-admin-commands.js +353 -105
- package/scripts/daemon-agent-commands.js +434 -66
- package/scripts/daemon-bridges.js +477 -68
- package/scripts/daemon-claude-engine.js +1267 -674
- package/scripts/daemon-command-router.js +205 -27
- package/scripts/daemon-command-session-route.js +118 -0
- package/scripts/daemon-default.yaml +7 -0
- package/scripts/daemon-engine-runtime.js +96 -20
- package/scripts/daemon-exec-commands.js +108 -49
- package/scripts/daemon-file-browser.js +64 -7
- package/scripts/daemon-notify.js +18 -4
- package/scripts/daemon-ops-commands.js +16 -2
- package/scripts/daemon-remote-dispatch.js +55 -1
- package/scripts/daemon-runtime-lifecycle.js +87 -0
- package/scripts/daemon-session-commands.js +102 -45
- package/scripts/daemon-session-store.js +497 -66
- package/scripts/daemon-siri-bridge.js +234 -0
- package/scripts/daemon-siri-imessage.js +209 -0
- package/scripts/daemon-task-scheduler.js +10 -2
- package/scripts/daemon.js +697 -179
- package/scripts/daemon.yaml +7 -0
- package/scripts/docs/agent-guide.md +36 -3
- package/scripts/docs/hook-config.md +134 -0
- package/scripts/docs/maintenance-manual.md +162 -5
- package/scripts/docs/pointer-map.md +60 -5
- package/scripts/feishu-adapter.js +7 -15
- package/scripts/hooks/doc-router.js +29 -0
- package/scripts/hooks/hook-utils.js +61 -0
- package/scripts/hooks/intent-doc-router.js +54 -0
- package/scripts/hooks/intent-engine.js +72 -0
- package/scripts/hooks/intent-file-transfer.js +51 -0
- package/scripts/hooks/intent-memory-recall.js +35 -0
- package/scripts/hooks/intent-ops-assist.js +54 -0
- package/scripts/hooks/intent-task-create.js +35 -0
- package/scripts/hooks/intent-team-dispatch.js +106 -0
- package/scripts/hooks/team-context.js +143 -0
- package/scripts/intent-registry.js +59 -0
- package/scripts/memory-extract.js +59 -0
- package/scripts/memory-nightly-reflect.js +109 -43
- package/scripts/memory.js +55 -17
- package/scripts/mentor-engine.js +6 -0
- package/scripts/schema.js +1 -0
- package/scripts/self-reflect.js +110 -12
- package/scripts/session-analytics.js +160 -0
- package/scripts/signal-capture.js +1 -1
- package/scripts/team-dispatch.js +315 -0
|
@@ -346,6 +346,65 @@ async function run() {
|
|
|
346
346
|
}
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
// ── Codex sessions ──────────────────────────────────────────────────────
|
|
350
|
+
// Same pipeline, different source: reads ~/.codex/sessions rollout files
|
|
351
|
+
// (first 2KB only) + history.jsonl for user messages.
|
|
352
|
+
const codexSessions = sessionAnalytics.findAllUnextractedCodexSessions(3);
|
|
353
|
+
if (codexSessions.length > 0) {
|
|
354
|
+
// Pass session IDs so loadCodexHistory only parses relevant entries
|
|
355
|
+
// (history.jsonl grows unbounded; no need to load the full file)
|
|
356
|
+
const historyMap = sessionAnalytics.loadCodexHistory(codexSessions.map(cs => cs.session_id));
|
|
357
|
+
for (const cs of codexSessions) {
|
|
358
|
+
try {
|
|
359
|
+
const { skeleton, evidence } = sessionAnalytics.buildCodexInput(cs.path, historyMap);
|
|
360
|
+
|
|
361
|
+
// Skip trivial sessions with no user messages
|
|
362
|
+
if (skeleton.message_count < 1) {
|
|
363
|
+
sessionAnalytics.markCodexFactsExtracted(cs.session_id);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const { ok, facts, session_name } = await extractFacts(skeleton, evidence, distillEnv);
|
|
368
|
+
if (!ok) {
|
|
369
|
+
console.log(`[memory-extract] Codex ${cs.session_id.slice(0, 8)}: extraction failed, will retry later`);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (facts.length > 0) {
|
|
374
|
+
const fallbackScope = `codex_${String(cs.session_id).replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 24)}`;
|
|
375
|
+
const { saved, skipped, superseded, savedFacts } = memory.saveFacts(
|
|
376
|
+
cs.session_id,
|
|
377
|
+
skeleton.project || 'unknown',
|
|
378
|
+
facts,
|
|
379
|
+
{ scope: skeleton.project_id || fallbackScope, source_type: 'codex' }
|
|
380
|
+
);
|
|
381
|
+
let labelsSaved = 0;
|
|
382
|
+
if (typeof memory.saveFactLabels === 'function' && Array.isArray(savedFacts) && savedFacts.length > 0) {
|
|
383
|
+
const labelRows = buildFactLabelRows(facts, savedFacts);
|
|
384
|
+
if (labelRows.length > 0) {
|
|
385
|
+
const lr = memory.saveFactLabels(labelRows);
|
|
386
|
+
labelsSaved = Number(lr && lr.saved) || 0;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
totalSaved += saved;
|
|
390
|
+
totalSkipped += skipped;
|
|
391
|
+
const superMsg = superseded > 0 ? `, ${superseded} superseded` : '';
|
|
392
|
+
const labelMsg = labelsSaved > 0 ? `, ${labelsSaved} labels` : '';
|
|
393
|
+
console.log(`[memory-extract] Codex ${cs.session_id.slice(0, 8)} (${session_name}): ${saved} facts saved${superMsg}${labelMsg}`);
|
|
394
|
+
} else {
|
|
395
|
+
console.log(`[memory-extract] Codex ${cs.session_id.slice(0, 8)} (${session_name}): no facts extracted`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
sessionAnalytics.markCodexFactsExtracted(cs.session_id);
|
|
399
|
+
saveSessionTag(cs.session_id, session_name, facts);
|
|
400
|
+
processed++;
|
|
401
|
+
} catch (e) {
|
|
402
|
+
console.log(`[memory-extract] Codex session error: ${e.message}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// ── end Codex ────────────────────────────────────────────────────────────
|
|
407
|
+
|
|
349
408
|
memory.close();
|
|
350
409
|
return { sessionsProcessed: processed, factsSaved: totalSaved, factsSkipped: totalSkipped };
|
|
351
410
|
} finally {
|
|
@@ -108,13 +108,13 @@ function writeReflectLog(record) {
|
|
|
108
108
|
* Query hot zone facts from memory.db.
|
|
109
109
|
* Returns array of plain objects.
|
|
110
110
|
*/
|
|
111
|
-
function queryHotFacts(db) {
|
|
111
|
+
function queryHotFacts(db, windowDays = WINDOW_DAYS) {
|
|
112
112
|
const relationPlaceholders = EXCLUDED_RELATIONS.map(() => '?').join(', ');
|
|
113
113
|
const stmt = db.prepare(`
|
|
114
114
|
SELECT id, entity, relation, value, confidence, search_count, created_at
|
|
115
115
|
FROM facts
|
|
116
116
|
WHERE search_count >= ${MIN_SEARCH_COUNT}
|
|
117
|
-
AND created_at >= datetime('now', '-${
|
|
117
|
+
AND created_at >= datetime('now', '-${windowDays} days')
|
|
118
118
|
AND superseded_by IS NULL
|
|
119
119
|
AND (conflict_status IS NULL OR conflict_status = 'OK')
|
|
120
120
|
AND relation NOT IN (${relationPlaceholders})
|
|
@@ -212,10 +212,8 @@ function parseJsonFromLlm(raw) {
|
|
|
212
212
|
try { return JSON.parse(cleaned); } catch { return null; }
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
function writeCapsuleFile(filePath,
|
|
216
|
-
const
|
|
217
|
-
const supporting = Array.isArray(capsule.supporting_facts) ? capsule.supporting_facts.slice(0, 8) : [];
|
|
218
|
-
const content = `---
|
|
215
|
+
function writeCapsuleFile(filePath, markdownContent, facts, today, prefix) {
|
|
216
|
+
const frontmatter = `---
|
|
219
217
|
date: ${today}
|
|
220
218
|
source: nightly-reflect
|
|
221
219
|
type: knowledge-capsule
|
|
@@ -223,21 +221,30 @@ entity_prefix: ${prefix}
|
|
|
223
221
|
facts_analyzed: ${Array.isArray(facts) ? facts.length : 0}
|
|
224
222
|
---
|
|
225
223
|
|
|
226
|
-
# ${capsule.title}
|
|
227
|
-
|
|
228
|
-
## 核心结论
|
|
229
|
-
${capsule.core_conclusion}
|
|
230
|
-
|
|
231
|
-
## 适用场景
|
|
232
|
-
${capsule.applicable_scenarios}
|
|
233
|
-
|
|
234
|
-
## 关联概念
|
|
235
|
-
${related.length > 0 ? related.map(x => `- ${x}`).join('\n') : '- (none)'}
|
|
236
|
-
|
|
237
|
-
## 支撑事实
|
|
238
|
-
${supporting.length > 0 ? supporting.map(x => `- ${x}`).join('\n') : '- (none)'}
|
|
239
224
|
`;
|
|
240
|
-
|
|
225
|
+
try {
|
|
226
|
+
fs.writeFileSync(filePath, frontmatter + markdownContent, 'utf8');
|
|
227
|
+
return true;
|
|
228
|
+
} catch (e) {
|
|
229
|
+
console.log(`[NIGHTLY-REFLECT] Warning: failed to write capsule ${filePath}: ${e.message}`);
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function appendCapsuleUpdate(filePath, markdownContent, today) {
|
|
235
|
+
// Idempotency: skip if same-day update already appended
|
|
236
|
+
try {
|
|
237
|
+
const existing = fs.readFileSync(filePath, 'utf8');
|
|
238
|
+
if (existing.includes(`## 🔄 增量研判 (${today})`)) return false;
|
|
239
|
+
} catch { /* file may not be readable, proceed */ }
|
|
240
|
+
try {
|
|
241
|
+
const section = `\n\n## 🔄 增量研判 (${today})\n\n${markdownContent}\n`;
|
|
242
|
+
fs.appendFileSync(filePath, section, 'utf8');
|
|
243
|
+
return true;
|
|
244
|
+
} catch (e) {
|
|
245
|
+
console.log(`[NIGHTLY-REFLECT] Warning: failed to append capsule update ${filePath}: ${e.message}`);
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
241
248
|
}
|
|
242
249
|
|
|
243
250
|
/**
|
|
@@ -272,7 +279,10 @@ async function run() {
|
|
|
272
279
|
db.exec('PRAGMA busy_timeout = 5000');
|
|
273
280
|
|
|
274
281
|
const hotFacts = queryHotFacts(db);
|
|
275
|
-
|
|
282
|
+
// Recent facts (last 1 day) used exclusively for incremental capsule appends,
|
|
283
|
+
// preventing the 7-day rolling window from re-distilling the same facts repeatedly.
|
|
284
|
+
const recentFacts = queryHotFacts(db, 1);
|
|
285
|
+
console.log(`[NIGHTLY-REFLECT] Found ${hotFacts.length} hot-zone facts (${recentFacts.length} from last 24h).`);
|
|
276
286
|
|
|
277
287
|
if (hotFacts.length < 3) {
|
|
278
288
|
console.log('[NIGHTLY-REFLECT] Insufficient hot facts (< 3), skipping distillation.');
|
|
@@ -332,6 +342,7 @@ Rules:
|
|
|
332
342
|
} catch (e) {
|
|
333
343
|
console.log(`[NIGHTLY-REFLECT] Haiku call failed: ${e.message}`);
|
|
334
344
|
writeReflectLog({ status: 'error', reason: 'haiku_failed', error: e.message, facts_found: hotFacts.length });
|
|
345
|
+
releaseLock();
|
|
335
346
|
return;
|
|
336
347
|
}
|
|
337
348
|
|
|
@@ -340,6 +351,7 @@ Rules:
|
|
|
340
351
|
if (!parsed || typeof parsed !== 'object') {
|
|
341
352
|
console.log('[NIGHTLY-REFLECT] Failed to parse Haiku output.');
|
|
342
353
|
writeReflectLog({ status: 'error', reason: 'parse_failed', facts_found: hotFacts.length });
|
|
354
|
+
releaseLock();
|
|
343
355
|
return;
|
|
344
356
|
}
|
|
345
357
|
|
|
@@ -377,50 +389,104 @@ Rules:
|
|
|
377
389
|
}
|
|
378
390
|
|
|
379
391
|
// 3C: knowledge capsule aggregation by entity prefix.
|
|
392
|
+
// Cold start uses full hotFacts (7 days); incremental uses recentFacts (1 day)
|
|
393
|
+
// to prevent the same facts from being re-distilled every night.
|
|
380
394
|
const capsuleGroups = collectCapsuleGroups(hotFacts, 3).slice(0, 3);
|
|
381
395
|
for (const group of capsuleGroups) {
|
|
382
|
-
const
|
|
396
|
+
const capsuleSlug = sanitizeSlug(group.prefix.replace(/\./g, '-'), 'capsule');
|
|
397
|
+
const capsuleFile = path.join(CAPSULES_DIR, `${capsuleSlug}-playbook.md`);
|
|
398
|
+
const playbookExists = fs.existsSync(capsuleFile);
|
|
399
|
+
|
|
400
|
+
// For incremental appends, only use facts from the last 24 hours
|
|
401
|
+
const factsForGroup = playbookExists
|
|
402
|
+
? collectCapsuleGroups(recentFacts, 1).find(g => g.prefix === group.prefix)
|
|
403
|
+
: group;
|
|
404
|
+
// Skip append if no new facts in the last 24 hours
|
|
405
|
+
if (playbookExists && !factsForGroup) continue;
|
|
406
|
+
|
|
407
|
+
const sourceItems = factsForGroup ? factsForGroup.items : group.items;
|
|
408
|
+
const groupFacts = sourceItems.map(f => ({
|
|
383
409
|
entity: f.entity,
|
|
384
410
|
relation: f.relation,
|
|
385
411
|
value: f.value,
|
|
386
412
|
search_count: f.search_count,
|
|
387
413
|
}));
|
|
388
|
-
const capsulePrompt =
|
|
414
|
+
const capsulePrompt = playbookExists
|
|
415
|
+
? `你是知识胶囊维护者。以下是该主题近期新增的原始事实,请提炼成简洁的增量段落(不超过300字),直接追加到现有手册。不要重复旧内容,不要输出大标题。
|
|
389
416
|
|
|
390
417
|
entity_prefix: ${group.prefix}
|
|
391
|
-
facts(json): ${JSON.stringify(groupFacts, null, 2).slice(0,
|
|
418
|
+
新增 facts(json): ${JSON.stringify(groupFacts, null, 2).slice(0, 3000)}
|
|
392
419
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
"core_conclusion":"一句核心结论",
|
|
397
|
-
"applicable_scenarios":"适用场景(1-2句)",
|
|
398
|
-
"related_concepts":["概念1","概念2"],
|
|
399
|
-
"supporting_facts":["支撑点1","支撑点2"]
|
|
400
|
-
}
|
|
420
|
+
输出格式(仅段落内容,Markdown 列表):
|
|
421
|
+
- **[具体要点/报错/红线]**:[原因与解法,含变量名/路径/报错原文]`
|
|
422
|
+
: `你是首席布道师与底层架构师。请将以下零散的开发者流水账,升维提炼成一本《硬核架构与避坑手册》(Playbook)。
|
|
401
423
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
- 每个字段简洁具体
|
|
405
|
-
- 仅输出 JSON`;
|
|
424
|
+
entity_prefix: ${group.prefix}
|
|
425
|
+
输入事实(JSON): ${JSON.stringify(groupFacts, null, 2).slice(0, 5000)}
|
|
406
426
|
|
|
407
|
-
|
|
427
|
+
【输出信仰与戒律】
|
|
428
|
+
1. 绝对的业务原子性:必须写具体的变量名、报错信息、文件路径,不要写"遇到了问题解决问题"。
|
|
429
|
+
2. 倒金字塔结构:致命错误、架构红线必须写在最前面的 🩸 血泪避坑指南 中。
|
|
430
|
+
3. 如果输入事实中包含报错原文(如 Exception 栈),必须保留在 🔍 历史报错指纹 中。
|
|
431
|
+
4. 必须使用 Markdown 格式输出,不要有任何 JSON 包装。
|
|
432
|
+
5. 去除一切废话:不要用"总而言之"、"这体现了"等水文词汇,全本必须干货。
|
|
433
|
+
|
|
434
|
+
请按以下 Markdown 模板输出:
|
|
435
|
+
# 📕 Playbook: [你提炼的主题名称]
|
|
436
|
+
|
|
437
|
+
> **胶囊摘要**:[一句话核心]
|
|
438
|
+
> **覆盖实体**:${group.prefix}
|
|
439
|
+
> **核心标签**:[tag1, tag2, tag3]
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## 1. 🩸 血泪避坑指南 (Critical Red Lines)
|
|
444
|
+
- **[问题名]**:[具体诱因与解法]
|
|
445
|
+
|
|
446
|
+
## 2. 🏗️ 架构决议 (Architecture Decisions)
|
|
447
|
+
- **[为什么选X不选Y]**:[理由]
|
|
448
|
+
|
|
449
|
+
## 3. 🔍 历史报错指纹 (Error Fingerprints)
|
|
450
|
+
- \`[报错原文]\`
|
|
451
|
+
- **诱因**:...
|
|
452
|
+
- **解法**:...
|
|
453
|
+
|
|
454
|
+
## 4. 🔗 图谱扩展 (Related Concepts)
|
|
455
|
+
- [相关主题或文件引用]`;
|
|
456
|
+
|
|
457
|
+
let capsuleMarkdown = null;
|
|
408
458
|
try {
|
|
409
459
|
const rawCapsule = await Promise.race([
|
|
410
460
|
callHaiku(capsulePrompt, distillEnv, 60000),
|
|
411
461
|
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 65000)),
|
|
412
462
|
]);
|
|
413
|
-
|
|
463
|
+
capsuleMarkdown = typeof rawCapsule === 'string' ? rawCapsule.trim() : null;
|
|
414
464
|
} catch { /* non-fatal */ }
|
|
415
|
-
|
|
465
|
+
// Strip markdown code fences that Haiku may wrap around output
|
|
466
|
+
if (capsuleMarkdown) {
|
|
467
|
+
capsuleMarkdown = capsuleMarkdown
|
|
468
|
+
.replace(/^```(markdown)?\s*/i, '')
|
|
469
|
+
.replace(/```\s*$/i, '')
|
|
470
|
+
.trim();
|
|
471
|
+
}
|
|
472
|
+
if (!capsuleMarkdown || capsuleMarkdown.length < 50) continue;
|
|
416
473
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
474
|
+
if (playbookExists) {
|
|
475
|
+
appendCapsuleUpdate(capsuleFile, capsuleMarkdown, today);
|
|
476
|
+
} else {
|
|
477
|
+
writeCapsuleFile(capsuleFile, capsuleMarkdown, sourceItems, today, group.prefix);
|
|
478
|
+
}
|
|
420
479
|
capsulesWritten++;
|
|
421
480
|
|
|
422
481
|
if (memory && typeof memory.saveFacts === 'function') {
|
|
423
|
-
const
|
|
482
|
+
const titleMatch = capsuleMarkdown.match(/^# 📕 Playbook:\s*(.+)$/m)
|
|
483
|
+
|| capsuleMarkdown.match(/^## (.+)$/m);
|
|
484
|
+
const capsuleTitle = (titleMatch ? titleMatch[1].trim() : group.prefix).slice(0, 80);
|
|
485
|
+
const summaryMatch = capsuleMarkdown.match(/\*\*胶囊摘要\*\*[::]\s*(.+)/);
|
|
486
|
+
const capsuleSummary = summaryMatch
|
|
487
|
+
? summaryMatch[1].trim()
|
|
488
|
+
: stripMd(capsuleMarkdown.slice(0, 120));
|
|
489
|
+
const capsuleValue = `${capsuleTitle}: ${capsuleSummary}`.slice(0, 280);
|
|
424
490
|
if (capsuleValue.length >= 20) {
|
|
425
491
|
const saveCapsule = memory.saveFacts(`capsule-${today}-${capsuleSlug}`, '*', [{
|
|
426
492
|
entity: `capsule.${group.prefix.replace(/\./g, '_')}`,
|
package/scripts/memory.js
CHANGED
|
@@ -99,8 +99,8 @@ function getDb() {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// Backward-compatible migration for old DBs without `scope`
|
|
102
|
-
try { _db.exec('ALTER TABLE sessions ADD COLUMN scope TEXT DEFAULT NULL'); } catch {}
|
|
103
|
-
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_scope ON sessions(scope)'); } catch {}
|
|
102
|
+
try { _db.exec('ALTER TABLE sessions ADD COLUMN scope TEXT DEFAULT NULL'); } catch { }
|
|
103
|
+
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_scope ON sessions(scope)'); } catch { }
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
// ── Facts table: atomic knowledge triples ──
|
|
@@ -167,27 +167,27 @@ function getDb() {
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
// Indexes
|
|
170
|
-
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_entity ON facts(entity)'); } catch {}
|
|
171
|
-
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_entity_relation ON facts(entity, relation)'); } catch {}
|
|
172
|
-
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_project ON facts(project)'); } catch {}
|
|
173
|
-
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_fact_labels_label ON fact_labels(label)'); } catch {}
|
|
174
|
-
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_fact_labels_domain ON fact_labels(domain)'); } catch {}
|
|
170
|
+
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_entity ON facts(entity)'); } catch { }
|
|
171
|
+
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_entity_relation ON facts(entity, relation)'); } catch { }
|
|
172
|
+
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_project ON facts(project)'); } catch { }
|
|
173
|
+
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_fact_labels_label ON fact_labels(label)'); } catch { }
|
|
174
|
+
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_fact_labels_domain ON fact_labels(domain)'); } catch { }
|
|
175
175
|
|
|
176
176
|
// Backward-compatible migration for old DBs without `scope`
|
|
177
|
-
try { _db.exec('ALTER TABLE facts ADD COLUMN scope TEXT DEFAULT NULL'); } catch {}
|
|
178
|
-
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_scope ON facts(scope)'); } catch {}
|
|
177
|
+
try { _db.exec('ALTER TABLE facts ADD COLUMN scope TEXT DEFAULT NULL'); } catch { }
|
|
178
|
+
try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_scope ON facts(scope)'); } catch { }
|
|
179
179
|
|
|
180
180
|
// Search frequency tracking: counts how many times a fact appeared in search results.
|
|
181
181
|
// This is a RELEVANCE PROXY, not a usefulness score — "searched" ≠ "actually helpful".
|
|
182
182
|
// Renamed from recall_count (was ambiguous). Migration copies existing data forward.
|
|
183
|
-
try { _db.exec('ALTER TABLE facts ADD COLUMN recall_count INTEGER DEFAULT 0'); } catch {}
|
|
184
|
-
try { _db.exec('ALTER TABLE facts ADD COLUMN search_count INTEGER DEFAULT 0'); } catch {}
|
|
185
|
-
try { _db.exec('ALTER TABLE facts ADD COLUMN last_searched_at TEXT'); } catch {}
|
|
183
|
+
try { _db.exec('ALTER TABLE facts ADD COLUMN recall_count INTEGER DEFAULT 0'); } catch { }
|
|
184
|
+
try { _db.exec('ALTER TABLE facts ADD COLUMN search_count INTEGER DEFAULT 0'); } catch { }
|
|
185
|
+
try { _db.exec('ALTER TABLE facts ADD COLUMN last_searched_at TEXT'); } catch { }
|
|
186
186
|
// One-time migration: copy recall_count → search_count for existing rows
|
|
187
|
-
try { _db.exec('UPDATE facts SET search_count = recall_count WHERE recall_count > 0 AND search_count = 0'); } catch {}
|
|
187
|
+
try { _db.exec('UPDATE facts SET search_count = recall_count WHERE recall_count > 0 AND search_count = 0'); } catch { }
|
|
188
188
|
|
|
189
189
|
// conflict_status: 'OK' (default) | 'CONFLICT' — set by _detectConflict for non-stateful relations
|
|
190
|
-
try { _db.exec("ALTER TABLE facts ADD COLUMN conflict_status TEXT NOT NULL DEFAULT 'OK'"); } catch {}
|
|
190
|
+
try { _db.exec("ALTER TABLE facts ADD COLUMN conflict_status TEXT NOT NULL DEFAULT 'OK'"); } catch { }
|
|
191
191
|
|
|
192
192
|
return _db;
|
|
193
193
|
}
|
|
@@ -317,8 +317,10 @@ function saveFacts(sessionId, project, facts, { scope = null } = {}) {
|
|
|
317
317
|
insert.run(id, f.entity, f.relation, f.value.slice(0, 300),
|
|
318
318
|
f.confidence || 'medium', sourceType, sessionId, normalizedProject, normalizedScope, tags);
|
|
319
319
|
batchDedup.add(dedupKey);
|
|
320
|
-
savedFacts.push({
|
|
321
|
-
|
|
320
|
+
savedFacts.push({
|
|
321
|
+
id, entity: f.entity, relation: f.relation, value: f.value,
|
|
322
|
+
project: normalizedProject, scope: normalizedScope, tags: f.tags || [], created_at: new Date().toISOString()
|
|
323
|
+
});
|
|
322
324
|
saved++;
|
|
323
325
|
|
|
324
326
|
// For stateful relations, mark older active facts with same entity::relation as superseded
|
|
@@ -440,7 +442,7 @@ function _trackSearch(ids) {
|
|
|
440
442
|
}
|
|
441
443
|
|
|
442
444
|
const SUPERSEDE_LOG = path.join(os.homedir(), '.metame', 'memory_supersede_log.jsonl');
|
|
443
|
-
const CONFLICT_LOG
|
|
445
|
+
const CONFLICT_LOG = path.join(os.homedir(), '.metame', 'memory_conflict_log.jsonl');
|
|
444
446
|
|
|
445
447
|
/**
|
|
446
448
|
* Append supersede operations to audit log (append-only, never mutated).
|
|
@@ -767,6 +769,42 @@ function searchFacts(query, { limit = 5, project = null, scope = null } = {}) {
|
|
|
767
769
|
}
|
|
768
770
|
} catch { /* fact_labels table may not exist yet */ }
|
|
769
771
|
}
|
|
772
|
+
// Entity path expansion: supplement with entity hierarchy matches per query term.
|
|
773
|
+
// Handles multi-word queries like "daemon dispatch" that won't match dotted entity paths
|
|
774
|
+
// (e.g. "MetaMe.daemon.dispatch.cross-bot") via LIKE '%daemon dispatch%'.
|
|
775
|
+
if (likeResults.length < limit) {
|
|
776
|
+
const terms = query.trim().split(/\s+/).filter(t => t.length >= 3);
|
|
777
|
+
if (terms.length > 0) {
|
|
778
|
+
try {
|
|
779
|
+
const likeIds = new Set(likeResults.map(r => r.id));
|
|
780
|
+
const entityOrClauses = terms.map(() => `entity LIKE ?`).join(' OR ');
|
|
781
|
+
const entityParams = terms.map(t => `%${t}%`);
|
|
782
|
+
let entityExpSql = `SELECT id, entity, relation, value, confidence, project, scope, tags, created_at
|
|
783
|
+
FROM facts WHERE (${entityOrClauses})
|
|
784
|
+
AND superseded_by IS NULL
|
|
785
|
+
AND (conflict_status IS NULL OR conflict_status NOT IN ('ARCHIVED', 'CONFLICT'))`;
|
|
786
|
+
if (scope && project) {
|
|
787
|
+
entityExpSql += ` AND ((scope = ? OR scope = '*') OR (scope IS NULL AND (project = ? OR project = '*')))`;
|
|
788
|
+
entityParams.push(scope, project);
|
|
789
|
+
} else if (scope) {
|
|
790
|
+
entityExpSql += ` AND (scope = ? OR scope = '*')`;
|
|
791
|
+
entityParams.push(scope);
|
|
792
|
+
} else if (project) {
|
|
793
|
+
entityExpSql += ` AND (project = ? OR project = '*')`;
|
|
794
|
+
entityParams.push(project);
|
|
795
|
+
}
|
|
796
|
+
entityExpSql += ` ORDER BY created_at DESC LIMIT ?`;
|
|
797
|
+
entityParams.push(limit);
|
|
798
|
+
const entityRows = db.prepare(entityExpSql).all(...entityParams);
|
|
799
|
+
for (const row of entityRows) {
|
|
800
|
+
if (!likeIds.has(row.id) && likeResults.length < limit) {
|
|
801
|
+
likeIds.add(row.id);
|
|
802
|
+
likeResults.push(row);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
} catch { /* entity expansion failed */ }
|
|
806
|
+
}
|
|
807
|
+
}
|
|
770
808
|
if (likeResults.length > 0) _trackSearch(likeResults.map(r => r.id));
|
|
771
809
|
return likeResults;
|
|
772
810
|
}
|
package/scripts/mentor-engine.js
CHANGED
|
@@ -54,6 +54,11 @@ function saveRuntime(runtime) {
|
|
|
54
54
|
fs.renameSync(tmp, file);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function clearRuntime() {
|
|
58
|
+
saveRuntime(defaultRuntime());
|
|
59
|
+
return defaultRuntime();
|
|
60
|
+
}
|
|
61
|
+
|
|
57
62
|
function normalizeText(input) {
|
|
58
63
|
return String(input || '').trim();
|
|
59
64
|
}
|
|
@@ -394,6 +399,7 @@ module.exports = {
|
|
|
394
399
|
gcExpiredDebts,
|
|
395
400
|
detectPatterns,
|
|
396
401
|
getRuntimeStatus,
|
|
402
|
+
clearRuntime,
|
|
397
403
|
_private: {
|
|
398
404
|
runtimeFilePath,
|
|
399
405
|
loadRuntime,
|
package/scripts/schema.js
CHANGED
|
@@ -103,6 +103,7 @@ const SCHEMA = {
|
|
|
103
103
|
|
|
104
104
|
// === T5: Growth (metacognition, system-managed) ===
|
|
105
105
|
'growth.patterns': { tier: 'T5', type: 'array', maxItems: 3 },
|
|
106
|
+
'growth.self_reflection_patterns': { tier: 'T5', type: 'array', maxItems: 3 },
|
|
106
107
|
'growth.zone_history': { tier: 'T5', type: 'array', maxItems: 10 },
|
|
107
108
|
'growth.reflections_answered': { tier: 'T5', type: 'number' },
|
|
108
109
|
'growth.reflections_skipped': { tier: 'T5', type: 'number' },
|
package/scripts/self-reflect.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Scans correction/metacognitive signals from the past 7 days,
|
|
7
7
|
* aggregates "where did the AI get it wrong", and writes a brief
|
|
8
|
-
* self-critique pattern into growth.
|
|
8
|
+
* self-critique pattern into growth.self_reflection_patterns
|
|
9
|
+
* in ~/.claude_profile.yaml.
|
|
9
10
|
*
|
|
10
11
|
* Also distills correction signals into lessons/ SOP markdown files.
|
|
11
12
|
*
|
|
@@ -27,6 +28,81 @@ const LOCK_FILE = path.join(HOME, '.metame', 'self-reflect.lock');
|
|
|
27
28
|
const LESSONS_DIR = path.join(HOME, '.metame', 'memory', 'lessons');
|
|
28
29
|
const WINDOW_DAYS = 7;
|
|
29
30
|
|
|
31
|
+
function normalizeReflectionEntry(entry) {
|
|
32
|
+
if (!entry) return null;
|
|
33
|
+
if (typeof entry === 'string') {
|
|
34
|
+
const summary = entry.trim();
|
|
35
|
+
if (!summary) return null;
|
|
36
|
+
return {
|
|
37
|
+
summary,
|
|
38
|
+
detected: new Date().toISOString().slice(0, 10),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (typeof entry === 'object' && typeof entry.summary === 'string' && entry.summary.trim()) {
|
|
42
|
+
return {
|
|
43
|
+
summary: entry.summary.trim(),
|
|
44
|
+
detected: entry.detected || new Date().toISOString().slice(0, 10),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function mergeReflectionEntries(entries) {
|
|
51
|
+
const merged = new Map();
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
const normalized = normalizeReflectionEntry(entry);
|
|
54
|
+
if (!normalized) continue;
|
|
55
|
+
const existing = merged.get(normalized.summary);
|
|
56
|
+
if (!existing) {
|
|
57
|
+
merged.set(normalized.summary, normalized);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const existingDetected = existing.detected ? new Date(existing.detected).getTime() : 0;
|
|
62
|
+
const normalizedDetected = normalized.detected ? new Date(normalized.detected).getTime() : 0;
|
|
63
|
+
const shouldReplace =
|
|
64
|
+
normalizedDetected > 0 && (
|
|
65
|
+
existingDetected === 0
|
|
66
|
+
|| normalizedDetected < existingDetected
|
|
67
|
+
);
|
|
68
|
+
if (shouldReplace) merged.set(normalized.summary, { ...existing, ...normalized });
|
|
69
|
+
}
|
|
70
|
+
return [...merged.values()];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeSelfReflectionPatterns(profile) {
|
|
74
|
+
if (!profile.growth) profile.growth = {};
|
|
75
|
+
|
|
76
|
+
const current = Array.isArray(profile.growth.self_reflection_patterns)
|
|
77
|
+
? profile.growth.self_reflection_patterns
|
|
78
|
+
: [];
|
|
79
|
+
const legacy = Array.isArray(profile.growth.patterns)
|
|
80
|
+
? profile.growth.patterns.filter(p => typeof p === 'string')
|
|
81
|
+
: [];
|
|
82
|
+
|
|
83
|
+
const normalized = mergeReflectionEntries([...current, ...legacy])
|
|
84
|
+
.slice(-3);
|
|
85
|
+
|
|
86
|
+
profile.growth.self_reflection_patterns = normalized;
|
|
87
|
+
if (Array.isArray(profile.growth.patterns)) {
|
|
88
|
+
profile.growth.patterns = profile.growth.patterns.filter(p => typeof p === 'object' && p && typeof p.summary === 'string');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return normalized;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function migrateLegacySelfReflectionPatterns(profile) {
|
|
95
|
+
const beforePatterns = JSON.stringify((profile.growth && profile.growth.patterns) || null);
|
|
96
|
+
const beforeReflections = JSON.stringify((profile.growth && profile.growth.self_reflection_patterns) || null);
|
|
97
|
+
const normalized = normalizeSelfReflectionPatterns(profile);
|
|
98
|
+
const afterPatterns = JSON.stringify((profile.growth && profile.growth.patterns) || null);
|
|
99
|
+
const afterReflections = JSON.stringify((profile.growth && profile.growth.self_reflection_patterns) || null);
|
|
100
|
+
return {
|
|
101
|
+
changed: beforePatterns !== afterPatterns || beforeReflections !== afterReflections,
|
|
102
|
+
normalized,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
30
106
|
/**
|
|
31
107
|
* Distill correction signals into reusable SOP markdown files.
|
|
32
108
|
* Each run produces at most one lesson file per unique slug.
|
|
@@ -148,6 +224,22 @@ async function run() {
|
|
|
148
224
|
}
|
|
149
225
|
|
|
150
226
|
try {
|
|
227
|
+
// Persist legacy string-pattern migration even when this run produces no new reflections.
|
|
228
|
+
if (fs.existsSync(BRAIN_FILE)) {
|
|
229
|
+
try {
|
|
230
|
+
const yaml = require('js-yaml');
|
|
231
|
+
const profile = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
|
|
232
|
+
const migrated = migrateLegacySelfReflectionPatterns(profile);
|
|
233
|
+
if (migrated.changed) {
|
|
234
|
+
const dumped = yaml.dump(profile, { lineWidth: -1 });
|
|
235
|
+
await writeBrainFileSafe(dumped);
|
|
236
|
+
console.log('[self-reflect] Migrated legacy growth.patterns strings into growth.self_reflection_patterns.');
|
|
237
|
+
}
|
|
238
|
+
} catch (e) {
|
|
239
|
+
console.log(`[self-reflect] Legacy pattern migration failed (non-fatal): ${e.message}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
151
243
|
// Read signals from last WINDOW_DAYS days
|
|
152
244
|
if (!fs.existsSync(SIGNAL_FILE)) {
|
|
153
245
|
console.log('[self-reflect] No signal file, skipping.');
|
|
@@ -175,9 +267,9 @@ async function run() {
|
|
|
175
267
|
try {
|
|
176
268
|
const yaml = require('js-yaml');
|
|
177
269
|
const profile = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
|
|
178
|
-
const existing = (profile.
|
|
270
|
+
const existing = migrateLegacySelfReflectionPatterns(profile).normalized;
|
|
179
271
|
if (existing.length > 0) {
|
|
180
|
-
currentPatterns = `Current growth.
|
|
272
|
+
currentPatterns = `Current growth.self_reflection_patterns (avoid repeating):\n${existing.map(p => `- ${p.summary}`).join('\n')}\n\n`;
|
|
181
273
|
}
|
|
182
274
|
} catch { /* non-fatal */ }
|
|
183
275
|
|
|
@@ -247,24 +339,24 @@ ${signalText}
|
|
|
247
339
|
return;
|
|
248
340
|
}
|
|
249
341
|
|
|
250
|
-
// Merge into growth.
|
|
342
|
+
// Merge into growth.self_reflection_patterns (cap at 3, keep newest)
|
|
251
343
|
try {
|
|
252
344
|
const yaml = require('js-yaml');
|
|
253
345
|
const raw = fs.readFileSync(BRAIN_FILE, 'utf8');
|
|
254
346
|
const profile = yaml.load(raw) || {};
|
|
255
347
|
if (!profile.growth) profile.growth = {};
|
|
256
|
-
const existing =
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
.
|
|
348
|
+
const existing = migrateLegacySelfReflectionPatterns(profile).normalized;
|
|
349
|
+
const merged = mergeReflectionEntries([
|
|
350
|
+
...existing,
|
|
351
|
+
...patterns.map(summary => ({ summary, detected: new Date().toISOString().slice(0, 10) })),
|
|
352
|
+
])
|
|
260
353
|
.slice(-3);
|
|
261
|
-
profile.growth.
|
|
354
|
+
profile.growth.self_reflection_patterns = merged;
|
|
262
355
|
profile.growth.last_reflection = new Date().toISOString().slice(0, 10);
|
|
263
356
|
|
|
264
|
-
// Preserve locked lines (simple approach: only update growth section)
|
|
265
357
|
const dumped = yaml.dump(profile, { lineWidth: -1 });
|
|
266
358
|
await writeBrainFileSafe(dumped);
|
|
267
|
-
console.log(`[self-reflect] ${patterns.length} pattern(s) written to growth.
|
|
359
|
+
console.log(`[self-reflect] ${patterns.length} pattern(s) written to growth.self_reflection_patterns: ${patterns.join(' | ')}`);
|
|
268
360
|
} catch (e) {
|
|
269
361
|
console.log(`[self-reflect] Failed to write profile: ${e.message}`);
|
|
270
362
|
}
|
|
@@ -283,4 +375,10 @@ if (require.main === module) {
|
|
|
283
375
|
});
|
|
284
376
|
}
|
|
285
377
|
|
|
286
|
-
module.exports = {
|
|
378
|
+
module.exports = {
|
|
379
|
+
run,
|
|
380
|
+
mergeReflectionEntries,
|
|
381
|
+
normalizeReflectionEntry,
|
|
382
|
+
normalizeSelfReflectionPatterns,
|
|
383
|
+
migrateLegacySelfReflectionPatterns,
|
|
384
|
+
};
|