hippo-memory 0.24.2 → 0.27.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/README.md +68 -54
- package/dist/audit.d.ts +17 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +107 -0
- package/dist/audit.js.map +1 -0
- package/dist/capture.d.ts.map +1 -1
- package/dist/capture.js +7 -4
- package/dist/capture.js.map +1 -1
- package/dist/cli.js +464 -35
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -1
- package/dist/consolidate.d.ts.map +1 -1
- package/dist/consolidate.js +75 -13
- package/dist/consolidate.js.map +1 -1
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/dashboard.js +102 -3
- package/dist/dashboard.js.map +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +12 -1
- package/dist/db.js.map +1 -1
- package/dist/eval.d.ts +68 -0
- package/dist/eval.d.ts.map +1 -0
- package/dist/eval.js +127 -0
- package/dist/eval.js.map +1 -0
- package/dist/memory.d.ts +2 -0
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +6 -0
- package/dist/memory.js.map +1 -1
- package/dist/search.d.ts +65 -0
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +155 -13
- package/dist/search.js.map +1 -1
- package/dist/shared.d.ts +3 -0
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +23 -7
- package/dist/shared.js.map +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +14 -4
- package/dist/store.js.map +1 -1
- package/dist-ui/assets/index-CxxqB9Wc.js +4308 -0
- package/dist-ui/index.html +52 -0
- package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +5 -2
package/dist/cli.js
CHANGED
|
@@ -46,6 +46,8 @@ import { getGlobalRoot, initGlobal, promoteToGlobal, shareMemory, listPeers, aut
|
|
|
46
46
|
import { DAILY_TASK_NAME, buildDailyRunnerCommand, listRegisteredWorkspaces, registerWorkspace, runDailyMaintenance, } from './scheduler.js';
|
|
47
47
|
import { importChatGPT, importClaude, importCursor, importGenericFile, importMarkdown, } from './importers.js';
|
|
48
48
|
import { cmdCapture } from './capture.js';
|
|
49
|
+
import { auditMemories } from './audit.js';
|
|
50
|
+
import { runEval, bootstrapCorpus } from './eval.js';
|
|
49
51
|
import { wmPush, wmRead, wmClear, wmFlush } from './working-memory.js';
|
|
50
52
|
// ---------------------------------------------------------------------------
|
|
51
53
|
// Helpers
|
|
@@ -438,6 +440,11 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
438
440
|
const config = loadConfig(hippoRoot);
|
|
439
441
|
const usePhysics = forcePhysics
|
|
440
442
|
|| (!forceClassic && config.physics.enabled !== false);
|
|
443
|
+
const noMmr = Boolean(flags['no-mmr']);
|
|
444
|
+
const mmrLambda = flags['mmr-lambda'] !== undefined
|
|
445
|
+
? parseFloat(String(flags['mmr-lambda']))
|
|
446
|
+
: config.mmr.lambda;
|
|
447
|
+
const mmrEnabled = !noMmr && config.mmr.enabled;
|
|
441
448
|
let results;
|
|
442
449
|
if (usePhysics && !hasGlobal) {
|
|
443
450
|
results = await physicsSearch(query, localEntries, {
|
|
@@ -448,10 +455,14 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
448
455
|
}
|
|
449
456
|
else if (hasGlobal) {
|
|
450
457
|
// Use searchBothHybrid for merged results with embedding support
|
|
451
|
-
results = await searchBothHybrid(query, hippoRoot, globalRoot, {
|
|
458
|
+
results = await searchBothHybrid(query, hippoRoot, globalRoot, {
|
|
459
|
+
budget, mmr: mmrEnabled, mmrLambda,
|
|
460
|
+
});
|
|
452
461
|
}
|
|
453
462
|
else {
|
|
454
|
-
results = await hybridSearch(query, localEntries, {
|
|
463
|
+
results = await hybridSearch(query, localEntries, {
|
|
464
|
+
budget, hippoRoot, mmr: mmrEnabled, mmrLambda,
|
|
465
|
+
});
|
|
455
466
|
}
|
|
456
467
|
if (limit < results.length) {
|
|
457
468
|
results = results.slice(0, limit);
|
|
@@ -523,6 +534,326 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
523
534
|
console.log();
|
|
524
535
|
}
|
|
525
536
|
}
|
|
537
|
+
async function cmdExplain(hippoRoot, query, flags) {
|
|
538
|
+
requireInit(hippoRoot);
|
|
539
|
+
const budget = parseInt(String(flags['budget'] ?? '4000'), 10);
|
|
540
|
+
const limit = parseLimitFlag(flags['limit']);
|
|
541
|
+
const asJson = Boolean(flags['json']);
|
|
542
|
+
const forcePhysics = Boolean(flags['physics']);
|
|
543
|
+
const forceClassic = Boolean(flags['classic']);
|
|
544
|
+
const globalRoot = getGlobalRoot();
|
|
545
|
+
const localEntries = loadSearchEntries(hippoRoot, query);
|
|
546
|
+
const globalEntries = isInitialized(globalRoot) ? loadSearchEntries(globalRoot, query) : [];
|
|
547
|
+
const hasGlobal = globalEntries.length > 0;
|
|
548
|
+
const config = loadConfig(hippoRoot);
|
|
549
|
+
const usePhysics = forcePhysics
|
|
550
|
+
|| (!forceClassic && config.physics.enabled !== false);
|
|
551
|
+
const noMmr = Boolean(flags['no-mmr']);
|
|
552
|
+
const mmrLambda = flags['mmr-lambda'] !== undefined
|
|
553
|
+
? parseFloat(String(flags['mmr-lambda']))
|
|
554
|
+
: config.mmr.lambda;
|
|
555
|
+
const mmrEnabled = !noMmr && config.mmr.enabled;
|
|
556
|
+
let results;
|
|
557
|
+
let modeUsed;
|
|
558
|
+
if (usePhysics && !hasGlobal) {
|
|
559
|
+
results = await physicsSearch(query, localEntries, {
|
|
560
|
+
budget,
|
|
561
|
+
hippoRoot,
|
|
562
|
+
physicsConfig: config.physics,
|
|
563
|
+
explain: true,
|
|
564
|
+
});
|
|
565
|
+
modeUsed = 'physics';
|
|
566
|
+
}
|
|
567
|
+
else if (hasGlobal) {
|
|
568
|
+
results = await searchBothHybrid(query, hippoRoot, globalRoot, {
|
|
569
|
+
budget, explain: true, mmr: mmrEnabled, mmrLambda,
|
|
570
|
+
});
|
|
571
|
+
modeUsed = 'searchBothHybrid';
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
results = await hybridSearch(query, localEntries, {
|
|
575
|
+
budget, hippoRoot, explain: true, mmr: mmrEnabled, mmrLambda,
|
|
576
|
+
});
|
|
577
|
+
modeUsed = 'hybrid';
|
|
578
|
+
}
|
|
579
|
+
if (limit < results.length) {
|
|
580
|
+
results = results.slice(0, limit);
|
|
581
|
+
}
|
|
582
|
+
const candidates = localEntries.length + globalEntries.length;
|
|
583
|
+
if (asJson) {
|
|
584
|
+
const output = results.map((r, rank) => ({
|
|
585
|
+
rank: rank + 1,
|
|
586
|
+
id: r.entry.id,
|
|
587
|
+
layer: r.entry.layer,
|
|
588
|
+
confidence: resolveConfidence(r.entry),
|
|
589
|
+
score: r.score,
|
|
590
|
+
tokens: r.tokens,
|
|
591
|
+
tags: r.entry.tags,
|
|
592
|
+
content: r.entry.content,
|
|
593
|
+
breakdown: r.breakdown,
|
|
594
|
+
}));
|
|
595
|
+
console.log(JSON.stringify({
|
|
596
|
+
query,
|
|
597
|
+
mode: modeUsed,
|
|
598
|
+
candidates,
|
|
599
|
+
returned: output.length,
|
|
600
|
+
results: output,
|
|
601
|
+
}));
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (results.length === 0) {
|
|
605
|
+
console.log(`No memories matched "${query}" (scanned ${candidates}).`);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
console.log(`Query: "${query}"`);
|
|
609
|
+
console.log(`Mode: ${modeUsed} candidates: ${candidates} returned: ${results.length}`);
|
|
610
|
+
console.log();
|
|
611
|
+
console.log('Rank Score Strength Age Layer ID Preview');
|
|
612
|
+
console.log('----- ------- --------- ------ ---------- ----------------- ---------------------------------');
|
|
613
|
+
for (let i = 0; i < results.length; i++) {
|
|
614
|
+
const r = results[i];
|
|
615
|
+
const b = r.breakdown;
|
|
616
|
+
const preview = r.entry.content.replace(/\s+/g, ' ').slice(0, 48);
|
|
617
|
+
const ageStr = b ? `${b.ageDays}d` : '?';
|
|
618
|
+
console.log(`${String(i + 1).padEnd(5)} ${fmt(r.score, 3).padEnd(7)} ${fmt(r.entry.strength).padEnd(9)} ${ageStr.padEnd(6)} ${r.entry.layer.padEnd(10)} ${r.entry.id.padEnd(17)} ${preview}`);
|
|
619
|
+
}
|
|
620
|
+
console.log();
|
|
621
|
+
for (let i = 0; i < results.length; i++) {
|
|
622
|
+
const r = results[i];
|
|
623
|
+
const b = r.breakdown;
|
|
624
|
+
console.log(`[${i + 1}] ${r.entry.id} composite=${fmt(r.score, 4)}`);
|
|
625
|
+
if (!b) {
|
|
626
|
+
console.log(' (no breakdown available)');
|
|
627
|
+
console.log();
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
if (b.mode === 'physics') {
|
|
631
|
+
console.log(` mode: physics-gravity`);
|
|
632
|
+
console.log(` cosine: ${fmt(b.cosine, 3)} (pre-amp baseline)`);
|
|
633
|
+
console.log(` final: ${fmt(b.final, 4)} (post-amp, from physics scorer)`);
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
const matched = b.matchedTerms.length > 0 ? b.matchedTerms.join(', ') : '(none)';
|
|
637
|
+
console.log(` mode: ${b.mode}${b.mode === 'hybrid-no-vec' ? ' (no cached doc vector — run `hippo embed`)' : ''}`);
|
|
638
|
+
console.log(` BM25: raw=${fmt(r.bm25, 3)} normalized=${fmt(b.normBm25, 3)} weight=${fmt(b.bm25Weight, 2)} matched=[${matched}]`);
|
|
639
|
+
console.log(` embedding: cosine=${fmt(b.cosine, 3)} weight=${fmt(b.embeddingWeight, 2)}`);
|
|
640
|
+
console.log(` base: ${fmt(b.bm25Weight, 2)}*${fmt(b.normBm25, 3)} + ${fmt(b.embeddingWeight, 2)}*${fmt(b.cosine, 3)} = ${fmt(b.base, 4)}`);
|
|
641
|
+
console.log(` strength: x${fmt(b.strengthMultiplier, 3)} (strength=${fmt(r.entry.strength, 3)})`);
|
|
642
|
+
console.log(` recency: x${fmt(b.recencyMultiplier, 3)} (age=${b.ageDays}d)`);
|
|
643
|
+
if (b.decisionBoost !== 1)
|
|
644
|
+
console.log(` decision: x${fmt(b.decisionBoost, 2)} (tagged 'decision')`);
|
|
645
|
+
if (b.pathBoost !== 1)
|
|
646
|
+
console.log(` path: x${fmt(b.pathBoost, 3)} (cwd path tag overlap)`);
|
|
647
|
+
if (b.sourceBump !== 1)
|
|
648
|
+
console.log(` source: x${fmt(b.sourceBump, 2)} (local priority bump over global)`);
|
|
649
|
+
if (b.outcomeBoost !== 1)
|
|
650
|
+
console.log(` outcome: x${fmt(b.outcomeBoost, 3)} (user feedback: pos-neg = ${(r.entry.outcome_positive ?? 0) - (r.entry.outcome_negative ?? 0)})`);
|
|
651
|
+
if (b.preMmrRank !== undefined && b.postMmrRank !== undefined && b.preMmrRank !== b.postMmrRank) {
|
|
652
|
+
const arrow = b.postMmrRank < b.preMmrRank ? 'up' : 'down';
|
|
653
|
+
console.log(` mmr: rank ${b.preMmrRank} -> ${b.postMmrRank} (diversity ${arrow})`);
|
|
654
|
+
}
|
|
655
|
+
console.log(` final: ${fmt(b.final, 4)}`);
|
|
656
|
+
}
|
|
657
|
+
console.log();
|
|
658
|
+
}
|
|
659
|
+
console.log('Note: explain does not mark memories as retrieved (read-only).');
|
|
660
|
+
}
|
|
661
|
+
async function cmdEval(hippoRoot, corpusPath, flags) {
|
|
662
|
+
requireInit(hippoRoot);
|
|
663
|
+
const asJson = Boolean(flags['json']);
|
|
664
|
+
const minMrr = flags['min-mrr'] !== undefined ? parseFloat(String(flags['min-mrr'])) : null;
|
|
665
|
+
const showCases = Boolean(flags['show-cases']);
|
|
666
|
+
const noMmr = Boolean(flags['no-mmr']);
|
|
667
|
+
const mmrLambda = flags['mmr-lambda'] !== undefined ? parseFloat(String(flags['mmr-lambda'])) : undefined;
|
|
668
|
+
const embeddingWeight = flags['embedding-weight'] !== undefined ? parseFloat(String(flags['embedding-weight'])) : undefined;
|
|
669
|
+
const entries = loadAllEntries(hippoRoot);
|
|
670
|
+
// Bootstrap mode: emit a synthetic corpus and exit.
|
|
671
|
+
if (flags['bootstrap']) {
|
|
672
|
+
const outPath = flags['out'] ? String(flags['out']) : null;
|
|
673
|
+
const max = flags['max-cases'] !== undefined ? parseInt(String(flags['max-cases']), 10) : 50;
|
|
674
|
+
const corpus = bootstrapCorpus(entries, max);
|
|
675
|
+
const payload = JSON.stringify({ cases: corpus }, null, 2);
|
|
676
|
+
if (outPath) {
|
|
677
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
678
|
+
fs.writeFileSync(outPath, payload, 'utf8');
|
|
679
|
+
console.log(`Wrote ${corpus.length} bootstrap cases to ${outPath}`);
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
console.log(payload);
|
|
683
|
+
}
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
if (!corpusPath) {
|
|
687
|
+
console.error('Usage: hippo eval <corpus.json> OR hippo eval --bootstrap [--out <path>]');
|
|
688
|
+
process.exit(1);
|
|
689
|
+
}
|
|
690
|
+
if (!fs.existsSync(corpusPath)) {
|
|
691
|
+
console.error(`Corpus file not found: ${corpusPath}`);
|
|
692
|
+
process.exit(1);
|
|
693
|
+
}
|
|
694
|
+
let cases;
|
|
695
|
+
try {
|
|
696
|
+
const raw = JSON.parse(fs.readFileSync(corpusPath, 'utf8'));
|
|
697
|
+
cases = Array.isArray(raw) ? raw : raw.cases;
|
|
698
|
+
if (!Array.isArray(cases))
|
|
699
|
+
throw new Error('Corpus JSON must be an array or { cases: [...] }');
|
|
700
|
+
}
|
|
701
|
+
catch (err) {
|
|
702
|
+
console.error(`Failed to read corpus: ${err instanceof Error ? err.message : err}`);
|
|
703
|
+
process.exit(1);
|
|
704
|
+
}
|
|
705
|
+
const summary = await runEval(cases, entries, {
|
|
706
|
+
hippoRoot,
|
|
707
|
+
mmr: !noMmr,
|
|
708
|
+
mmrLambda,
|
|
709
|
+
embeddingWeight,
|
|
710
|
+
});
|
|
711
|
+
if (asJson) {
|
|
712
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
console.log(`Eval: ${summary.cases.length} cases, ${summary.durationMs}ms`);
|
|
716
|
+
console.log();
|
|
717
|
+
console.log(`MRR: ${fmt(summary.meanMrr, 4)}`);
|
|
718
|
+
console.log(`Recall@5: ${fmt(summary.meanRecallAt5, 4)}`);
|
|
719
|
+
console.log(`Recall@10: ${fmt(summary.meanRecallAt10, 4)}`);
|
|
720
|
+
console.log(`NDCG@10: ${fmt(summary.meanNdcgAt10, 4)}`);
|
|
721
|
+
if (showCases) {
|
|
722
|
+
console.log();
|
|
723
|
+
console.log('Case details:');
|
|
724
|
+
for (const c of summary.cases) {
|
|
725
|
+
const exp = c.case.expectedIds.length;
|
|
726
|
+
const expectedSet = new Set(c.case.expectedIds);
|
|
727
|
+
const hitTop10 = c.returnedIds.slice(0, 10).filter((id) => expectedSet.has(id));
|
|
728
|
+
const missed = c.case.expectedIds.filter((id) => !c.returnedIds.slice(0, 10).includes(id));
|
|
729
|
+
console.log();
|
|
730
|
+
console.log(`[${c.case.id}] R@10=${fmt(c.recallAt10, 2)} MRR=${fmt(c.mrr, 2)} expected=${exp} hit=${hitTop10.length}`);
|
|
731
|
+
console.log(` query: ${c.case.query}`);
|
|
732
|
+
console.log(` top 3: ${c.returnedIds.slice(0, 3).join(', ') || '(none)'}`);
|
|
733
|
+
if (missed.length > 0) {
|
|
734
|
+
const shown = missed.slice(0, 4);
|
|
735
|
+
const more = missed.length > shown.length ? ` +${missed.length - shown.length} more` : '';
|
|
736
|
+
console.log(` missed: ${shown.join(', ')}${more}`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
console.log();
|
|
741
|
+
const failing = summary.cases.filter((c) => c.mrr === 0);
|
|
742
|
+
if (failing.length > 0) {
|
|
743
|
+
console.log(`${failing.length} case(s) returned zero relevant results:`);
|
|
744
|
+
for (const f of failing.slice(0, 10)) {
|
|
745
|
+
console.log(` [${f.case.id}] "${f.case.query.slice(0, 60)}"`);
|
|
746
|
+
}
|
|
747
|
+
if (failing.length > 10)
|
|
748
|
+
console.log(` ...and ${failing.length - 10} more`);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (minMrr !== null && summary.meanMrr < minMrr) {
|
|
752
|
+
console.error(`MRR ${fmt(summary.meanMrr, 4)} below threshold ${minMrr}`);
|
|
753
|
+
process.exit(1);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
function cmdTrace(hippoRoot, id, flags) {
|
|
757
|
+
requireInit(hippoRoot);
|
|
758
|
+
const asJson = Boolean(flags['json']);
|
|
759
|
+
// Look in local store first, then global.
|
|
760
|
+
let entry = readEntry(hippoRoot, id);
|
|
761
|
+
let sourceLabel = 'local';
|
|
762
|
+
const globalRoot = getGlobalRoot();
|
|
763
|
+
if (!entry && isInitialized(globalRoot)) {
|
|
764
|
+
entry = readEntry(globalRoot, id);
|
|
765
|
+
sourceLabel = 'global';
|
|
766
|
+
}
|
|
767
|
+
if (!entry) {
|
|
768
|
+
console.error(`Memory not found: ${id}`);
|
|
769
|
+
process.exit(1);
|
|
770
|
+
}
|
|
771
|
+
const now = new Date();
|
|
772
|
+
const strength = calculateStrength(entry, now);
|
|
773
|
+
const halfLife = deriveHalfLife(7, entry);
|
|
774
|
+
const rewardFactor = calculateRewardFactor(entry);
|
|
775
|
+
const effHalfLife = halfLife * rewardFactor;
|
|
776
|
+
const createdMs = new Date(entry.created).getTime();
|
|
777
|
+
const ageDays = (now.getTime() - createdMs) / 86_400_000;
|
|
778
|
+
const lastMs = new Date(entry.last_retrieved).getTime();
|
|
779
|
+
const sinceLast = (now.getTime() - lastMs) / 86_400_000;
|
|
780
|
+
const conf = resolveConfidence(entry, now);
|
|
781
|
+
// Projected strength: same decay curve, just push `now` out.
|
|
782
|
+
const projectedAt = (days) => calculateStrength(entry, new Date(now.getTime() + days * 86_400_000));
|
|
783
|
+
// Parents (consolidation lineage) — schema v9 field.
|
|
784
|
+
const parents = Array.isArray(entry.parents) ? entry.parents : [];
|
|
785
|
+
const parentPreviews = parents.map((pid) => {
|
|
786
|
+
const p = readEntry(hippoRoot, pid) ?? (isInitialized(globalRoot) ? readEntry(globalRoot, pid) : null);
|
|
787
|
+
return { id: pid, content: p ? p.content.replace(/\s+/g, ' ').slice(0, 70) : '(not found)' };
|
|
788
|
+
});
|
|
789
|
+
// Open conflicts involving this memory.
|
|
790
|
+
const allConflicts = [
|
|
791
|
+
...listMemoryConflicts(hippoRoot, 'open'),
|
|
792
|
+
...(isInitialized(globalRoot) ? listMemoryConflicts(globalRoot, 'open') : []),
|
|
793
|
+
];
|
|
794
|
+
const myConflicts = allConflicts.filter((c) => c.memory_a_id === id || c.memory_b_id === id);
|
|
795
|
+
if (asJson) {
|
|
796
|
+
console.log(JSON.stringify({
|
|
797
|
+
id: entry.id,
|
|
798
|
+
source: sourceLabel,
|
|
799
|
+
layer: entry.layer,
|
|
800
|
+
confidence: conf,
|
|
801
|
+
pinned: entry.pinned,
|
|
802
|
+
starred: entry.starred,
|
|
803
|
+
tags: entry.tags,
|
|
804
|
+
content: entry.content,
|
|
805
|
+
created: entry.created,
|
|
806
|
+
age_days: ageDays,
|
|
807
|
+
last_retrieved: entry.last_retrieved,
|
|
808
|
+
days_since_last_retrieval: sinceLast,
|
|
809
|
+
retrieval_count: entry.retrieval_count,
|
|
810
|
+
strength_now: strength,
|
|
811
|
+
half_life_days: halfLife,
|
|
812
|
+
reward_factor: rewardFactor,
|
|
813
|
+
effective_half_life_days: effHalfLife,
|
|
814
|
+
projected_strength_30d: projectedAt(30),
|
|
815
|
+
projected_strength_90d: projectedAt(90),
|
|
816
|
+
outcome_positive: entry.outcome_positive,
|
|
817
|
+
outcome_negative: entry.outcome_negative,
|
|
818
|
+
parents: parentPreviews,
|
|
819
|
+
open_conflicts: myConflicts,
|
|
820
|
+
}, null, 2));
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
console.log(`Memory: ${entry.id} [${sourceLabel}]`);
|
|
824
|
+
console.log('='.repeat(50));
|
|
825
|
+
console.log(`Content: ${entry.content.replace(/\s+/g, ' ').slice(0, 160)}${entry.content.length > 160 ? '...' : ''}`);
|
|
826
|
+
console.log(`Layer: ${entry.layer.padEnd(10)} Confidence: ${conf.padEnd(10)} Pinned: ${entry.pinned ? 'yes' : 'no'}${entry.starred ? ' Starred: yes' : ''}`);
|
|
827
|
+
console.log(`Tags: ${entry.tags.join(', ') || '(none)'}`);
|
|
828
|
+
console.log(`Created: ${entry.created} (${fmt(ageDays, 1)} days ago)`);
|
|
829
|
+
console.log();
|
|
830
|
+
console.log(`Strength trajectory:`);
|
|
831
|
+
console.log(` now: ${fmt(strength, 3)}`);
|
|
832
|
+
console.log(` in 30 days: ${fmt(projectedAt(30), 3)}`);
|
|
833
|
+
console.log(` in 90 days: ${fmt(projectedAt(90), 3)}`);
|
|
834
|
+
console.log(` half-life: ${fmt(halfLife, 1)}d (base) x ${fmt(rewardFactor, 2)} reward = ${fmt(effHalfLife, 1)}d effective`);
|
|
835
|
+
console.log();
|
|
836
|
+
console.log(`Retrieval:`);
|
|
837
|
+
console.log(` count: ${entry.retrieval_count}`);
|
|
838
|
+
console.log(` last: ${entry.last_retrieved} (${fmt(sinceLast, 1)} days ago)`);
|
|
839
|
+
console.log();
|
|
840
|
+
console.log(`Outcomes: +${entry.outcome_positive} / -${entry.outcome_negative}`);
|
|
841
|
+
if (parentPreviews.length > 0) {
|
|
842
|
+
console.log();
|
|
843
|
+
console.log(`Parents (consolidation lineage):`);
|
|
844
|
+
for (const p of parentPreviews) {
|
|
845
|
+
console.log(` - ${p.id}: ${p.content}`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (myConflicts.length > 0) {
|
|
849
|
+
console.log();
|
|
850
|
+
console.log(`Open conflicts: ${myConflicts.length}`);
|
|
851
|
+
for (const c of myConflicts) {
|
|
852
|
+
const other = c.memory_a_id === id ? c.memory_b_id : c.memory_a_id;
|
|
853
|
+
console.log(` - with ${other}: ${c.reason} (score=${fmt(c.score, 2)})`);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
526
857
|
/**
|
|
527
858
|
* Scan for Claude Code MEMORY.md files and import new entries into hippo.
|
|
528
859
|
* Looks in ~/.claude/projects/<project>/memory/ for .md files with YAML frontmatter.
|
|
@@ -767,6 +1098,24 @@ function cmdSleepCore(hippoRoot, flags) {
|
|
|
767
1098
|
console.log(`\nDeduped ${dedupResult.removed} duplicates (${parts.join(', ')}). Kept stronger copies.`);
|
|
768
1099
|
}
|
|
769
1100
|
}
|
|
1101
|
+
// Quality audit — remove junk, report warnings
|
|
1102
|
+
if (!dryRun) {
|
|
1103
|
+
const allEntries = loadAllEntries(hippoRoot);
|
|
1104
|
+
const audit = auditMemories(allEntries);
|
|
1105
|
+
if (audit.issues.length > 0) {
|
|
1106
|
+
const errors = audit.issues.filter(i => i.severity === 'error');
|
|
1107
|
+
const warnings = audit.issues.filter(i => i.severity === 'warning');
|
|
1108
|
+
if (errors.length > 0) {
|
|
1109
|
+
for (const issue of errors) {
|
|
1110
|
+
deleteEntry(hippoRoot, issue.memoryId);
|
|
1111
|
+
}
|
|
1112
|
+
console.log(`\nAudit: removed ${errors.length} junk memories (too short/empty).`);
|
|
1113
|
+
}
|
|
1114
|
+
if (warnings.length > 0) {
|
|
1115
|
+
console.log(`Audit: ${warnings.length} low-quality memories detected (run \`hippo audit\` for details).`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
770
1119
|
// Auto-share high-transfer-score memories to global (unless --no-share or dry-run)
|
|
771
1120
|
if (!dryRun && !flags['no-share']) {
|
|
772
1121
|
const sleepConfig = loadConfig(hippoRoot);
|
|
@@ -2150,7 +2499,7 @@ hippo capture --stdin <<< '<decisions, errors, lessons — 2-5 bullets>'
|
|
|
2150
2499
|
'codex': {
|
|
2151
2500
|
file: 'AGENTS.md',
|
|
2152
2501
|
description: 'OpenAI Codex',
|
|
2153
|
-
content: `
|
|
2502
|
+
content: `
|
|
2154
2503
|
## Project Memory (Hippo)
|
|
2155
2504
|
|
|
2156
2505
|
At the start of every task, run:
|
|
@@ -2169,8 +2518,8 @@ On task completion:
|
|
|
2169
2518
|
hippo outcome --good
|
|
2170
2519
|
\`\`\`
|
|
2171
2520
|
|
|
2172
|
-
When Hippo's Codex wrapper is installed, session-end capture runs automatically.
|
|
2173
|
-
If the wrapper is not installed, capture a brief summary manually:
|
|
2521
|
+
When Hippo's Codex wrapper is installed, session-end capture runs automatically.
|
|
2522
|
+
If the wrapper is not installed, capture a brief summary manually:
|
|
2174
2523
|
\`\`\`bash
|
|
2175
2524
|
hippo capture --stdin <<< '<decisions, errors, lessons — 2-5 bullets>'
|
|
2176
2525
|
\`\`\`
|
|
@@ -2602,13 +2951,13 @@ Hippo - biologically-inspired memory system for AI agents
|
|
|
2602
2951
|
Usage: hippo <command> [options]
|
|
2603
2952
|
|
|
2604
2953
|
Commands:
|
|
2605
|
-
init Create .hippo/ structure in current directory
|
|
2606
|
-
--scan [dir] Find all git repos under dir (default: ~) and init each
|
|
2607
|
-
--days <n> Days of git history to seed (default: 365 for --scan, 30 for single)
|
|
2608
|
-
--global Init the global store ($HIPPO_HOME or ~/.hippo/)
|
|
2609
|
-
--no-hooks Skip auto-detecting and installing agent hooks
|
|
2610
|
-
--no-schedule Skip auto-creating the machine-level daily runner
|
|
2611
|
-
--no-learn Skip seeding memories from git history
|
|
2954
|
+
init Create .hippo/ structure in current directory
|
|
2955
|
+
--scan [dir] Find all git repos under dir (default: ~) and init each
|
|
2956
|
+
--days <n> Days of git history to seed (default: 365 for --scan, 30 for single)
|
|
2957
|
+
--global Init the global store ($HIPPO_HOME or ~/.hippo/)
|
|
2958
|
+
--no-hooks Skip auto-detecting and installing agent hooks
|
|
2959
|
+
--no-schedule Skip auto-creating the machine-level daily runner
|
|
2960
|
+
--no-learn Skip seeding memories from git history
|
|
2612
2961
|
remember <text> Store a memory
|
|
2613
2962
|
--tag <tag> Add a tag (repeatable)
|
|
2614
2963
|
--error Tag as error (boosts retention)
|
|
@@ -2621,20 +2970,43 @@ Commands:
|
|
|
2621
2970
|
--budget <n> Token budget (default: 4000)
|
|
2622
2971
|
--json Output as JSON
|
|
2623
2972
|
--why Show match reasons and source annotations
|
|
2624
|
-
|
|
2973
|
+
--no-mmr Disable MMR diversity re-ranking
|
|
2974
|
+
--mmr-lambda <f> MMR balance 0..1 (default: 0.7, 1.0 = pure relevance)
|
|
2975
|
+
explain <query> Show full score breakdown for each retrieved memory
|
|
2976
|
+
--budget <n> Token budget (default: 4000)
|
|
2977
|
+
--limit <n> Cap the number of results displayed
|
|
2978
|
+
--json Output as JSON
|
|
2979
|
+
--physics | --classic Force search mode (default: from config)
|
|
2980
|
+
--no-mmr Disable MMR diversity re-ranking
|
|
2981
|
+
--mmr-lambda <f> MMR balance 0..1 (default: 0.7, 1.0 = pure relevance)
|
|
2982
|
+
trace <id> Memory dossier: content, decay trajectory, retrievals,
|
|
2983
|
+
outcomes, consolidation parents, open conflicts
|
|
2984
|
+
--json Output as JSON
|
|
2985
|
+
eval [<corpus.json>] Measure recall quality against a test corpus
|
|
2986
|
+
--bootstrap Generate a synthetic corpus from current memories
|
|
2987
|
+
--out <path> With --bootstrap, write to file instead of stdout
|
|
2988
|
+
--max-cases <n> With --bootstrap, cap case count (default: 50)
|
|
2989
|
+
--show-cases Print per-case details (query, R@10, missed, top 3)
|
|
2990
|
+
--no-mmr Disable MMR for this eval run
|
|
2991
|
+
--mmr-lambda <f> Override MMR lambda for this run
|
|
2992
|
+
--embedding-weight <f> Override cosine weight (default: 0.6)
|
|
2993
|
+
--min-mrr <f> Exit non-zero if mean MRR falls below this
|
|
2994
|
+
--json Output full summary as JSON
|
|
2995
|
+
context Smart context injection for AI agents
|
|
2625
2996
|
--auto Auto-detect task from git state
|
|
2626
2997
|
--budget <n> Token budget (default: 1500)
|
|
2627
2998
|
--format <fmt> Output format: markdown (default) or json
|
|
2628
2999
|
--framing <mode> Framing: observe (default), suggest, assert
|
|
2629
|
-
sleep Run consolidation pass (auto-learns + dedup + auto-shares)
|
|
2630
|
-
--dry-run Preview without writing
|
|
2631
|
-
--no-learn Skip auto git-learn before consolidation
|
|
2632
|
-
--no-share Skip auto-sharing to global store
|
|
2633
|
-
daily-runner Sweep registered workspaces and run daily learn+sleep
|
|
2634
|
-
dedup Remove duplicate memories (keeps stronger copy)
|
|
3000
|
+
sleep Run consolidation pass (auto-learns + dedup + auto-shares)
|
|
3001
|
+
--dry-run Preview without writing
|
|
3002
|
+
--no-learn Skip auto git-learn before consolidation
|
|
3003
|
+
--no-share Skip auto-sharing to global store
|
|
3004
|
+
daily-runner Sweep registered workspaces and run daily learn+sleep
|
|
3005
|
+
dedup Remove duplicate memories (keeps stronger copy)
|
|
2635
3006
|
--dry-run Preview without removing
|
|
2636
3007
|
--threshold <n> Overlap threshold 0-1 (default: 0.7)
|
|
2637
3008
|
status Show memory health stats
|
|
3009
|
+
audit [--fix] Check memory quality (--fix removes junk)
|
|
2638
3010
|
outcome Apply feedback to last recall
|
|
2639
3011
|
--good Memories were helpful
|
|
2640
3012
|
--bad Memories were irrelevant
|
|
@@ -2723,20 +3095,20 @@ Commands:
|
|
|
2723
3095
|
--log-file <path> Tee output to a log file (paired with 'hippo last-sleep')
|
|
2724
3096
|
--dry-run Preview without writing
|
|
2725
3097
|
--global Write to global store ($HIPPO_HOME or ~/.hippo/)
|
|
2726
|
-
setup One-shot: detect installed AI tools and install all
|
|
2727
|
-
available SessionEnd+SessionStart hooks
|
|
2728
|
-
--all Install for every JSON-hook tool, even if not detected
|
|
2729
|
-
--dry-run Show what would be installed without writing
|
|
2730
|
-
--no-schedule Skip installing or repairing the daily runner
|
|
2731
|
-
last-sleep Print the last 'hippo sleep --log-file' output and clear it
|
|
2732
|
-
--path <p> Log path (default: ~/.hippo/logs/last-sleep.log)
|
|
2733
|
-
--keep Print without clearing
|
|
2734
|
-
codex-run [-- ...args] Launch real Codex behind Hippo's session-end wrapper
|
|
2735
|
-
hook <sub> [target] Manage framework integrations
|
|
2736
|
-
hook list Show available hooks
|
|
2737
|
-
hook install <target> Install hook (claude-code|codex|cursor|openclaw|opencode|pi)
|
|
2738
|
-
claude-code/opencode install SessionEnd+SessionStart;
|
|
2739
|
-
codex wraps the detected launcher in place
|
|
3098
|
+
setup One-shot: detect installed AI tools and install all
|
|
3099
|
+
available SessionEnd+SessionStart hooks
|
|
3100
|
+
--all Install for every JSON-hook tool, even if not detected
|
|
3101
|
+
--dry-run Show what would be installed without writing
|
|
3102
|
+
--no-schedule Skip installing or repairing the daily runner
|
|
3103
|
+
last-sleep Print the last 'hippo sleep --log-file' output and clear it
|
|
3104
|
+
--path <p> Log path (default: ~/.hippo/logs/last-sleep.log)
|
|
3105
|
+
--keep Print without clearing
|
|
3106
|
+
codex-run [-- ...args] Launch real Codex behind Hippo's session-end wrapper
|
|
3107
|
+
hook <sub> [target] Manage framework integrations
|
|
3108
|
+
hook list Show available hooks
|
|
3109
|
+
hook install <target> Install hook (claude-code|codex|cursor|openclaw|opencode|pi)
|
|
3110
|
+
claude-code/opencode install SessionEnd+SessionStart;
|
|
3111
|
+
codex wraps the detected launcher in place
|
|
2740
3112
|
hook uninstall <target> Remove hook
|
|
2741
3113
|
decide "<decision>" Record an architectural decision (90-day half-life)
|
|
2742
3114
|
--context "<why>" Why this decision was made
|
|
@@ -2805,8 +3177,8 @@ async function main() {
|
|
|
2805
3177
|
break;
|
|
2806
3178
|
case 'remember': {
|
|
2807
3179
|
const text = args.join(' ').trim();
|
|
2808
|
-
if (!text) {
|
|
2809
|
-
console.error('
|
|
3180
|
+
if (!text || text.length < 3) {
|
|
3181
|
+
console.error('Memory content too short (minimum 3 characters).');
|
|
2810
3182
|
process.exit(1);
|
|
2811
3183
|
}
|
|
2812
3184
|
cmdRemember(hippoRoot, text, flags);
|
|
@@ -2821,6 +3193,29 @@ async function main() {
|
|
|
2821
3193
|
await cmdRecall(hippoRoot, query, flags);
|
|
2822
3194
|
break;
|
|
2823
3195
|
}
|
|
3196
|
+
case 'explain': {
|
|
3197
|
+
const query = args.join(' ').trim();
|
|
3198
|
+
if (!query) {
|
|
3199
|
+
console.error('Please provide a search query.');
|
|
3200
|
+
process.exit(1);
|
|
3201
|
+
}
|
|
3202
|
+
await cmdExplain(hippoRoot, query, flags);
|
|
3203
|
+
break;
|
|
3204
|
+
}
|
|
3205
|
+
case 'eval': {
|
|
3206
|
+
const corpusPath = args[0] ? String(args[0]) : null;
|
|
3207
|
+
await cmdEval(hippoRoot, corpusPath, flags);
|
|
3208
|
+
break;
|
|
3209
|
+
}
|
|
3210
|
+
case 'trace': {
|
|
3211
|
+
const id = args[0] ? String(args[0]) : null;
|
|
3212
|
+
if (!id) {
|
|
3213
|
+
console.error('Usage: hippo trace <memory-id>');
|
|
3214
|
+
process.exit(1);
|
|
3215
|
+
}
|
|
3216
|
+
cmdTrace(hippoRoot, id, flags);
|
|
3217
|
+
break;
|
|
3218
|
+
}
|
|
2824
3219
|
case 'sleep':
|
|
2825
3220
|
cmdSleep(hippoRoot, flags);
|
|
2826
3221
|
break;
|
|
@@ -2842,6 +3237,40 @@ async function main() {
|
|
|
2842
3237
|
case 'dedup':
|
|
2843
3238
|
cmdDedup(hippoRoot, flags);
|
|
2844
3239
|
break;
|
|
3240
|
+
case 'audit': {
|
|
3241
|
+
requireInit(hippoRoot);
|
|
3242
|
+
const entries = loadAllEntries(hippoRoot);
|
|
3243
|
+
const result = auditMemories(entries);
|
|
3244
|
+
const shouldFix = Boolean(flags['fix']);
|
|
3245
|
+
if (result.issues.length === 0) {
|
|
3246
|
+
console.log(`All ${result.total} memories passed quality checks.`);
|
|
3247
|
+
}
|
|
3248
|
+
else {
|
|
3249
|
+
console.log(`Audited ${result.total} memories: ${result.clean} clean, ${result.issues.length} issues\n`);
|
|
3250
|
+
for (const issue of result.issues) {
|
|
3251
|
+
const icon = issue.severity === 'error' ? 'ERR' : 'WARN';
|
|
3252
|
+
console.log(` [${icon}] ${issue.memoryId}: ${issue.reason}`);
|
|
3253
|
+
console.log(` "${issue.content.slice(0, 80)}${issue.content.length > 80 ? '...' : ''}"`);
|
|
3254
|
+
}
|
|
3255
|
+
if (shouldFix) {
|
|
3256
|
+
const errorIds = result.issues.filter(i => i.severity === 'error').map(i => i.memoryId);
|
|
3257
|
+
if (errorIds.length > 0) {
|
|
3258
|
+
for (const id of errorIds) {
|
|
3259
|
+
deleteEntry(hippoRoot, id);
|
|
3260
|
+
}
|
|
3261
|
+
console.log(`\nRemoved ${errorIds.length} error-severity memories.`);
|
|
3262
|
+
console.log(`${result.issues.length - errorIds.length} warnings remain (review manually).`);
|
|
3263
|
+
}
|
|
3264
|
+
else {
|
|
3265
|
+
console.log(`\nNo error-severity issues. Warnings require manual review.`);
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
else {
|
|
3269
|
+
console.log(`\nRun with --fix to auto-remove error-severity issues.`);
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
break;
|
|
3273
|
+
}
|
|
2845
3274
|
case 'status':
|
|
2846
3275
|
cmdStatus(hippoRoot);
|
|
2847
3276
|
break;
|