hippo-memory 0.26.0 → 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 +8 -0
- package/dist/cli.js +377 -2
- 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/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/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/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 +1 -1
package/README.md
CHANGED
|
@@ -60,6 +60,14 @@ hippo recall "data pipeline issues" --budget 2000
|
|
|
60
60
|
|
|
61
61
|
---
|
|
62
62
|
|
|
63
|
+
### What's new in v0.27.0
|
|
64
|
+
|
|
65
|
+
- **Recall is now debuggable.** `hippo explain <query>` prints the full score breakdown for each retrieved memory: BM25 + cosine, every multiplier (strength, recency, decision, path, source-bump, outcome), age, and final composite. Read-only so it's safe to run as a diagnostic.
|
|
66
|
+
- **`hippo trace <id>`** gives a one-page dossier per memory: decay trajectory projected to 30/90 days, effective half-life, retrieval staleness, outcome counts, consolidation parents, open conflicts.
|
|
67
|
+
- **MMR diversity** re-ranks near-duplicate results so you don't get five paraphrases at the top. Default `lambda=0.7`, tunable via config or `--no-mmr` / `--mmr-lambda`.
|
|
68
|
+
- **Outcome feedback is immediate.** `hippo outcome --good` now nudges that memory up on the very next recall (not just via slow half-life decay). Bounded at +/-15%.
|
|
69
|
+
- **`hippo eval`** measures recall quality against a test corpus (MRR, Recall@K, NDCG@K). Gate CI with `--min-mrr`. A real 15-case corpus ships at `evals/real-corpus.json`; baseline numbers in `evals/README.md`.
|
|
70
|
+
|
|
63
71
|
### What's new in v0.26.0
|
|
64
72
|
|
|
65
73
|
- **`hippo audit` catches junk memories.** New command flags too-short entries, release/merge/WIP commit noise, fragments, and vague single-clause notes. `--fix` removes the worst offenders. `hippo sleep` now runs audit automatically so commit-noise never survives consolidation.
|
package/dist/cli.js
CHANGED
|
@@ -47,6 +47,7 @@ import { DAILY_TASK_NAME, buildDailyRunnerCommand, listRegisteredWorkspaces, reg
|
|
|
47
47
|
import { importChatGPT, importClaude, importCursor, importGenericFile, importMarkdown, } from './importers.js';
|
|
48
48
|
import { cmdCapture } from './capture.js';
|
|
49
49
|
import { auditMemories } from './audit.js';
|
|
50
|
+
import { runEval, bootstrapCorpus } from './eval.js';
|
|
50
51
|
import { wmPush, wmRead, wmClear, wmFlush } from './working-memory.js';
|
|
51
52
|
// ---------------------------------------------------------------------------
|
|
52
53
|
// Helpers
|
|
@@ -439,6 +440,11 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
439
440
|
const config = loadConfig(hippoRoot);
|
|
440
441
|
const usePhysics = forcePhysics
|
|
441
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;
|
|
442
448
|
let results;
|
|
443
449
|
if (usePhysics && !hasGlobal) {
|
|
444
450
|
results = await physicsSearch(query, localEntries, {
|
|
@@ -449,10 +455,14 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
449
455
|
}
|
|
450
456
|
else if (hasGlobal) {
|
|
451
457
|
// Use searchBothHybrid for merged results with embedding support
|
|
452
|
-
results = await searchBothHybrid(query, hippoRoot, globalRoot, {
|
|
458
|
+
results = await searchBothHybrid(query, hippoRoot, globalRoot, {
|
|
459
|
+
budget, mmr: mmrEnabled, mmrLambda,
|
|
460
|
+
});
|
|
453
461
|
}
|
|
454
462
|
else {
|
|
455
|
-
results = await hybridSearch(query, localEntries, {
|
|
463
|
+
results = await hybridSearch(query, localEntries, {
|
|
464
|
+
budget, hippoRoot, mmr: mmrEnabled, mmrLambda,
|
|
465
|
+
});
|
|
456
466
|
}
|
|
457
467
|
if (limit < results.length) {
|
|
458
468
|
results = results.slice(0, limit);
|
|
@@ -524,6 +534,326 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
524
534
|
console.log();
|
|
525
535
|
}
|
|
526
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
|
+
}
|
|
527
857
|
/**
|
|
528
858
|
* Scan for Claude Code MEMORY.md files and import new entries into hippo.
|
|
529
859
|
* Looks in ~/.claude/projects/<project>/memory/ for .md files with YAML frontmatter.
|
|
@@ -2640,6 +2970,28 @@ Commands:
|
|
|
2640
2970
|
--budget <n> Token budget (default: 4000)
|
|
2641
2971
|
--json Output as JSON
|
|
2642
2972
|
--why Show match reasons and source annotations
|
|
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
|
|
2643
2995
|
context Smart context injection for AI agents
|
|
2644
2996
|
--auto Auto-detect task from git state
|
|
2645
2997
|
--budget <n> Token budget (default: 1500)
|
|
@@ -2841,6 +3193,29 @@ async function main() {
|
|
|
2841
3193
|
await cmdRecall(hippoRoot, query, flags);
|
|
2842
3194
|
break;
|
|
2843
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
|
+
}
|
|
2844
3219
|
case 'sleep':
|
|
2845
3220
|
cmdSleep(hippoRoot, flags);
|
|
2846
3221
|
break;
|