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.
Files changed (48) hide show
  1. package/README.md +68 -54
  2. package/dist/audit.d.ts +17 -0
  3. package/dist/audit.d.ts.map +1 -0
  4. package/dist/audit.js +107 -0
  5. package/dist/audit.js.map +1 -0
  6. package/dist/capture.d.ts.map +1 -1
  7. package/dist/capture.js +7 -4
  8. package/dist/capture.js.map +1 -1
  9. package/dist/cli.js +464 -35
  10. package/dist/cli.js.map +1 -1
  11. package/dist/config.d.ts +8 -0
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +5 -0
  14. package/dist/config.js.map +1 -1
  15. package/dist/consolidate.d.ts.map +1 -1
  16. package/dist/consolidate.js +75 -13
  17. package/dist/consolidate.js.map +1 -1
  18. package/dist/dashboard.d.ts.map +1 -1
  19. package/dist/dashboard.js +102 -3
  20. package/dist/dashboard.js.map +1 -1
  21. package/dist/db.d.ts.map +1 -1
  22. package/dist/db.js +12 -1
  23. package/dist/db.js.map +1 -1
  24. package/dist/eval.d.ts +68 -0
  25. package/dist/eval.d.ts.map +1 -0
  26. package/dist/eval.js +127 -0
  27. package/dist/eval.js.map +1 -0
  28. package/dist/memory.d.ts +2 -0
  29. package/dist/memory.d.ts.map +1 -1
  30. package/dist/memory.js +6 -0
  31. package/dist/memory.js.map +1 -1
  32. package/dist/search.d.ts +65 -0
  33. package/dist/search.d.ts.map +1 -1
  34. package/dist/search.js +155 -13
  35. package/dist/search.js.map +1 -1
  36. package/dist/shared.d.ts +3 -0
  37. package/dist/shared.d.ts.map +1 -1
  38. package/dist/shared.js +23 -7
  39. package/dist/shared.js.map +1 -1
  40. package/dist/store.d.ts.map +1 -1
  41. package/dist/store.js +14 -4
  42. package/dist/store.js.map +1 -1
  43. package/dist-ui/assets/index-CxxqB9Wc.js +4308 -0
  44. package/dist-ui/index.html +52 -0
  45. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  46. package/extensions/openclaw-plugin/package.json +1 -1
  47. package/openclaw.plugin.json +1 -1
  48. 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, { budget });
458
+ results = await searchBothHybrid(query, hippoRoot, globalRoot, {
459
+ budget, mmr: mmrEnabled, mmrLambda,
460
+ });
452
461
  }
453
462
  else {
454
- results = await hybridSearch(query, localEntries, { budget, hippoRoot });
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
- context Smart context injection for AI agents
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('Please provide text to remember.');
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;