hippo-memory 0.31.0 ā 0.33.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 +16 -0
- package/dist/cli.js +213 -19
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -1
- package/dist/consolidate.d.ts +5 -1
- package/dist/consolidate.d.ts.map +1 -1
- package/dist/consolidate.js +66 -10
- package/dist/consolidate.js.map +1 -1
- package/dist/dag.d.ts +20 -0
- package/dist/dag.d.ts.map +1 -0
- package/dist/dag.js +104 -0
- package/dist/dag.js.map +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +36 -1
- package/dist/db.js.map +1 -1
- package/dist/extract.d.ts +14 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +87 -0
- package/dist/extract.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/memory.d.ts +9 -0
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +5 -0
- package/dist/memory.js.map +1 -1
- package/dist/multihop.d.ts +11 -0
- package/dist/multihop.d.ts.map +1 -0
- package/dist/multihop.js +32 -0
- package/dist/multihop.js.map +1 -0
- package/dist/search.d.ts +15 -0
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +188 -7
- package/dist/search.js.map +1 -1
- package/dist/shared.d.ts +4 -0
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +3 -3
- package/dist/shared.js.map +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +23 -3
- package/dist/store.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,22 @@ hippo recall "data pipeline issues" --budget 2000
|
|
|
60
60
|
|
|
61
61
|
---
|
|
62
62
|
|
|
63
|
+
### What's new in v0.33.0
|
|
64
|
+
|
|
65
|
+
- **Fact extraction at sleep time.** `hippo sleep` now extracts standalone facts from episodic memories via LLM, stored as semantic-layer entries that score 1.3x higher and auto-deduplicate against their raw source in search results.
|
|
66
|
+
- **DAG summarization.** Extracted facts cluster by entity similarity into summary nodes. When a summary matches your query, its children drill down into results automatically.
|
|
67
|
+
- **Multi-hop retrieval.** `hippo recall --multihop` chains two search passes via entity tags discovered in the first pass, finding connections that single-pass search misses.
|
|
68
|
+
- **`hippo dag --stats`** shows how your memory is organized across DAG levels.
|
|
69
|
+
- **Performance fix.** Temporal scoring refactored from O(N^2) to O(N), eliminating stack overflow risk on large stores.
|
|
70
|
+
|
|
71
|
+
### What's new in v0.32.0
|
|
72
|
+
|
|
73
|
+
- **Correction without deletion.** `hippo supersede <old-id> "<new content>"` links the old memory as historical truth and creates a successor. Default recall shows only current beliefs; the old one stays in the store so you can audit what changed and when.
|
|
74
|
+
- **`--include-superseded`** on `recall` and `explain` surfaces historical memories with a `[superseded]` marker. Useful for "what did I used to think about X?"
|
|
75
|
+
- **`--as-of <ISO-date>`** returns the set of beliefs that were current at a past moment. Invalid dates exit with a clear format hint.
|
|
76
|
+
- **Schema v11, zero breaking changes.** Adds `valid_from` + `superseded_by` columns. Existing v10 stores upgrade on first open, no data loss, no manual migration.
|
|
77
|
+
- **Physics search ablation: CUT.** Benchmarked over 60 LongMemEval-oracle questions: physics-on is statistically worse than plain BM25 + embeddings on MRR, Recall@5, and NDCG@5 (paired bootstrap, 5000 iters, 95% CI excludes zero). Full results in `benchmarks/physics-ablation/`. Physics stays in the codebase this release; removal is a separate decision.
|
|
78
|
+
|
|
63
79
|
### What's new in v0.31.0
|
|
64
80
|
|
|
65
81
|
- **Scope-aware corrections.** Tag a memory with `hippo remember --scope plan-eng-review` and it only surfaces strongly when that scope is active again. Matching scope gets 1.5x boost, mismatching scope is suppressed 0.5x, unscoped memories stay neutral. Corrections said during one skill stop polluting unrelated contexts.
|
package/dist/cli.js
CHANGED
|
@@ -53,6 +53,7 @@ import { auditMemories } from './audit.js';
|
|
|
53
53
|
import { runEval, bootstrapCorpus, compareSummaries } from './eval.js';
|
|
54
54
|
import { refineStore } from './refine-llm.js';
|
|
55
55
|
import { wmPush, wmRead, wmClear, wmFlush } from './working-memory.js';
|
|
56
|
+
import { multihopSearch } from './multihop.js';
|
|
56
57
|
// ---------------------------------------------------------------------------
|
|
57
58
|
// Helpers
|
|
58
59
|
// ---------------------------------------------------------------------------
|
|
@@ -377,7 +378,7 @@ function setupDailySchedule(globalRoot) {
|
|
|
377
378
|
}
|
|
378
379
|
}
|
|
379
380
|
}
|
|
380
|
-
function cmdRemember(hippoRoot, text, flags) {
|
|
381
|
+
async function cmdRemember(hippoRoot, text, flags) {
|
|
381
382
|
const useGlobal = Boolean(flags['global']);
|
|
382
383
|
const targetRoot = useGlobal ? getGlobalRoot() : hippoRoot;
|
|
383
384
|
if (useGlobal) {
|
|
@@ -437,6 +438,59 @@ function cmdRemember(hippoRoot, text, flags) {
|
|
|
437
438
|
// Silently ignore embedding errors
|
|
438
439
|
});
|
|
439
440
|
}
|
|
441
|
+
const config = loadConfig(targetRoot);
|
|
442
|
+
const shouldExtract = flags['extract'] || config.extraction.enabled === true;
|
|
443
|
+
const apiKey = process.env.ANTHROPIC_API_KEY ?? '';
|
|
444
|
+
if (shouldExtract && apiKey) {
|
|
445
|
+
try {
|
|
446
|
+
const { extractFacts, storeExtractedFacts } = await import('./extract.js');
|
|
447
|
+
const facts = await extractFacts(entry.content, {
|
|
448
|
+
apiKey,
|
|
449
|
+
model: config.extraction.model,
|
|
450
|
+
});
|
|
451
|
+
if (facts.length > 0) {
|
|
452
|
+
storeExtractedFacts(targetRoot, entry, facts);
|
|
453
|
+
console.error(` extracted ${facts.length} fact(s)`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
// Extraction is best-effort ā never block remember
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
else if (shouldExtract && !apiKey) {
|
|
461
|
+
console.error(' (extraction skipped: ANTHROPIC_API_KEY not set)');
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function cmdSupersede(hippoRoot, oldId, newContent, flags) {
|
|
465
|
+
requireInit(hippoRoot);
|
|
466
|
+
const old = readEntry(hippoRoot, oldId);
|
|
467
|
+
if (!old) {
|
|
468
|
+
console.error(`Error: memory ${oldId} not found.`);
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
471
|
+
if (old.superseded_by) {
|
|
472
|
+
console.error(`Error: memory ${oldId} is already superseded by ${old.superseded_by}. Supersede that one instead.`);
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
const layer = (typeof flags['layer'] === 'string' ? flags['layer'] : old.layer);
|
|
476
|
+
const rawTags = flags['tag'];
|
|
477
|
+
const tags = Array.isArray(rawTags)
|
|
478
|
+
? rawTags.map((t) => String(t))
|
|
479
|
+
: typeof rawTags === 'string'
|
|
480
|
+
? rawTags.split(',').map((t) => t.trim()).filter(Boolean)
|
|
481
|
+
: [...old.tags];
|
|
482
|
+
const pinned = flags['pin'] === true || old.pinned;
|
|
483
|
+
const newEntry = createMemory(newContent, {
|
|
484
|
+
layer,
|
|
485
|
+
tags,
|
|
486
|
+
pinned,
|
|
487
|
+
source: old.source,
|
|
488
|
+
confidence: 'verified',
|
|
489
|
+
});
|
|
490
|
+
old.superseded_by = newEntry.id;
|
|
491
|
+
writeEntry(hippoRoot, old);
|
|
492
|
+
writeEntry(hippoRoot, newEntry);
|
|
493
|
+
console.log(`Superseded ${oldId} ā ${newEntry.id}`);
|
|
440
494
|
}
|
|
441
495
|
async function cmdRecall(hippoRoot, query, flags) {
|
|
442
496
|
requireInit(hippoRoot);
|
|
@@ -446,9 +500,43 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
446
500
|
const showWhy = Boolean(flags['why']);
|
|
447
501
|
const forcePhysics = Boolean(flags['physics']);
|
|
448
502
|
const forceClassic = Boolean(flags['classic']);
|
|
503
|
+
const includeSuperseded = Boolean(flags['include-superseded']);
|
|
504
|
+
const asOf = typeof flags['as-of'] === 'string' ? flags['as-of'] : undefined;
|
|
505
|
+
if (asOf !== undefined && Number.isNaN(new Date(asOf).getTime())) {
|
|
506
|
+
console.error(`Error: --as-of value "${asOf}" is not a valid ISO date (e.g. 2026-04-22 or 2026-04-22T12:00:00Z).`);
|
|
507
|
+
process.exit(1);
|
|
508
|
+
}
|
|
449
509
|
const globalRoot = getGlobalRoot();
|
|
450
|
-
|
|
451
|
-
|
|
510
|
+
let localEntries = loadSearchEntries(hippoRoot, query);
|
|
511
|
+
let globalEntries = isInitialized(globalRoot) ? loadSearchEntries(globalRoot, query) : [];
|
|
512
|
+
// Bi-temporal filtering for physics path (hybridSearch handles it internally)
|
|
513
|
+
if (asOf) {
|
|
514
|
+
const filterAsOf = (entries) => {
|
|
515
|
+
const asOfDate = new Date(asOf);
|
|
516
|
+
const successorValidFrom = new Map();
|
|
517
|
+
for (const e of entries) {
|
|
518
|
+
if (e.superseded_by) {
|
|
519
|
+
const successor = entries.find(s => s.id === e.superseded_by);
|
|
520
|
+
if (successor)
|
|
521
|
+
successorValidFrom.set(e.id, successor.valid_from);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return entries.filter(e => {
|
|
525
|
+
if (new Date(e.valid_from) > asOfDate)
|
|
526
|
+
return false;
|
|
527
|
+
if (!e.superseded_by)
|
|
528
|
+
return true;
|
|
529
|
+
const succVf = successorValidFrom.get(e.id);
|
|
530
|
+
return succVf ? new Date(succVf) > asOfDate : true;
|
|
531
|
+
});
|
|
532
|
+
};
|
|
533
|
+
localEntries = filterAsOf(localEntries);
|
|
534
|
+
globalEntries = filterAsOf(globalEntries);
|
|
535
|
+
}
|
|
536
|
+
else if (!includeSuperseded) {
|
|
537
|
+
localEntries = localEntries.filter(e => !e.superseded_by);
|
|
538
|
+
globalEntries = globalEntries.filter(e => !e.superseded_by);
|
|
539
|
+
}
|
|
452
540
|
const hasGlobal = globalEntries.length > 0;
|
|
453
541
|
// Determine search mode: --physics forces physics, --classic forces BM25+cosine,
|
|
454
542
|
// default uses physics if config.physics.enabled is not false
|
|
@@ -470,8 +558,19 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
470
558
|
: undefined;
|
|
471
559
|
const recallExplicitScope = flags['scope'] !== undefined ? String(flags['scope']).trim() : null;
|
|
472
560
|
const recallActiveScope = recallExplicitScope || detectScope();
|
|
561
|
+
const useMultihop = flags['multihop'] === true || config.multihop.enabled;
|
|
473
562
|
let results;
|
|
474
|
-
if (
|
|
563
|
+
if (useMultihop) {
|
|
564
|
+
const allEntries = [...localEntries, ...globalEntries];
|
|
565
|
+
results = multihopSearch(query, allEntries, {
|
|
566
|
+
budget,
|
|
567
|
+
hippoRoot,
|
|
568
|
+
minResults,
|
|
569
|
+
includeSuperseded,
|
|
570
|
+
asOf,
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
else if (usePhysics && !hasGlobal) {
|
|
475
574
|
results = await physicsSearch(query, localEntries, {
|
|
476
575
|
budget,
|
|
477
576
|
hippoRoot,
|
|
@@ -555,6 +654,10 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
555
654
|
if (r.entry.layer === Layer.Trace) {
|
|
556
655
|
base.trace_outcome = r.entry.trace_outcome;
|
|
557
656
|
}
|
|
657
|
+
if (r.entry.superseded_by) {
|
|
658
|
+
base.superseded = true;
|
|
659
|
+
base.superseded_by = r.entry.superseded_by;
|
|
660
|
+
}
|
|
558
661
|
if (showWhy) {
|
|
559
662
|
const explanation = explainMatch(query, r);
|
|
560
663
|
base.confidence = resolveConfidence(r.entry);
|
|
@@ -577,8 +680,9 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
577
680
|
const strengthBar = '\u2588'.repeat(Math.round(e.strength * 10)) + '\u2591'.repeat(10 - Math.round(e.strength * 10));
|
|
578
681
|
const isGlobal = isInitialized(globalRoot) && !localIndex.entries[e.id];
|
|
579
682
|
const globalMark = isGlobal ? ' [global]' : '';
|
|
683
|
+
const supersededMark = e.superseded_by ? ' [superseded]' : '';
|
|
580
684
|
const sourceMark = isGlobal ? ' [global]' : ' [local]';
|
|
581
|
-
console.log(`--- ${e.id} [${e.layer}] ${confLabel}${globalMark} score=${fmt(r.score, 3)} strength=${fmt(e.strength)}`);
|
|
685
|
+
console.log(`--- ${e.id} [${e.layer}] ${confLabel}${globalMark}${supersededMark} score=${fmt(r.score, 3)} strength=${fmt(e.strength)}`);
|
|
582
686
|
console.log(` [${strengthBar}] tags: ${e.tags.join(', ') || 'none'} | retrieved: ${e.retrieval_count}x`);
|
|
583
687
|
if (showWhy) {
|
|
584
688
|
const explanation = explainMatch(query, r);
|
|
@@ -597,10 +701,44 @@ async function cmdExplain(hippoRoot, query, flags) {
|
|
|
597
701
|
const asJson = Boolean(flags['json']);
|
|
598
702
|
const forcePhysics = Boolean(flags['physics']);
|
|
599
703
|
const forceClassic = Boolean(flags['classic']);
|
|
704
|
+
const explainIncludeSuperseded = Boolean(flags['include-superseded']);
|
|
705
|
+
const explainAsOf = typeof flags['as-of'] === 'string' ? flags['as-of'] : undefined;
|
|
706
|
+
if (explainAsOf !== undefined && Number.isNaN(new Date(explainAsOf).getTime())) {
|
|
707
|
+
console.error(`Error: --as-of value "${explainAsOf}" is not a valid ISO date (e.g. 2026-04-22 or 2026-04-22T12:00:00Z).`);
|
|
708
|
+
process.exit(1);
|
|
709
|
+
}
|
|
600
710
|
const globalRoot = getGlobalRoot();
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
711
|
+
let explainLocalEntries = loadSearchEntries(hippoRoot, query);
|
|
712
|
+
let explainGlobalEntries = isInitialized(globalRoot) ? loadSearchEntries(globalRoot, query) : [];
|
|
713
|
+
// Bi-temporal filtering
|
|
714
|
+
if (explainAsOf) {
|
|
715
|
+
const filterAsOfExplain = (entries) => {
|
|
716
|
+
const asOfDate = new Date(explainAsOf);
|
|
717
|
+
const successorValidFrom = new Map();
|
|
718
|
+
for (const e of entries) {
|
|
719
|
+
if (e.superseded_by) {
|
|
720
|
+
const successor = entries.find(s => s.id === e.superseded_by);
|
|
721
|
+
if (successor)
|
|
722
|
+
successorValidFrom.set(e.id, successor.valid_from);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return entries.filter(e => {
|
|
726
|
+
if (new Date(e.valid_from) > asOfDate)
|
|
727
|
+
return false;
|
|
728
|
+
if (!e.superseded_by)
|
|
729
|
+
return true;
|
|
730
|
+
const succVf = successorValidFrom.get(e.id);
|
|
731
|
+
return succVf ? new Date(succVf) > asOfDate : true;
|
|
732
|
+
});
|
|
733
|
+
};
|
|
734
|
+
explainLocalEntries = filterAsOfExplain(explainLocalEntries);
|
|
735
|
+
explainGlobalEntries = filterAsOfExplain(explainGlobalEntries);
|
|
736
|
+
}
|
|
737
|
+
else if (!explainIncludeSuperseded) {
|
|
738
|
+
explainLocalEntries = explainLocalEntries.filter(e => !e.superseded_by);
|
|
739
|
+
explainGlobalEntries = explainGlobalEntries.filter(e => !e.superseded_by);
|
|
740
|
+
}
|
|
741
|
+
const hasGlobal = explainGlobalEntries.length > 0;
|
|
604
742
|
const config = loadConfig(hippoRoot);
|
|
605
743
|
const usePhysics = forcePhysics
|
|
606
744
|
|| (!forceClassic && config.physics.enabled !== false);
|
|
@@ -619,7 +757,7 @@ async function cmdExplain(hippoRoot, query, flags) {
|
|
|
619
757
|
let results;
|
|
620
758
|
let modeUsed;
|
|
621
759
|
if (usePhysics && !hasGlobal) {
|
|
622
|
-
results = await physicsSearch(query,
|
|
760
|
+
results = await physicsSearch(query, explainLocalEntries, {
|
|
623
761
|
budget,
|
|
624
762
|
hippoRoot,
|
|
625
763
|
physicsConfig: config.physics,
|
|
@@ -631,19 +769,21 @@ async function cmdExplain(hippoRoot, query, flags) {
|
|
|
631
769
|
else if (hasGlobal) {
|
|
632
770
|
results = await searchBothHybrid(query, hippoRoot, globalRoot, {
|
|
633
771
|
budget, explain: true, mmr: mmrEnabled, mmrLambda, localBump, scope: explainActiveScope,
|
|
772
|
+
includeSuperseded: explainIncludeSuperseded, asOf: explainAsOf,
|
|
634
773
|
});
|
|
635
774
|
modeUsed = 'searchBothHybrid';
|
|
636
775
|
}
|
|
637
776
|
else {
|
|
638
|
-
results = await hybridSearch(query,
|
|
777
|
+
results = await hybridSearch(query, explainLocalEntries, {
|
|
639
778
|
budget, hippoRoot, explain: true, mmr: mmrEnabled, mmrLambda, scope: explainActiveScope,
|
|
779
|
+
includeSuperseded: explainIncludeSuperseded, asOf: explainAsOf,
|
|
640
780
|
});
|
|
641
781
|
modeUsed = 'hybrid';
|
|
642
782
|
}
|
|
643
783
|
if (limit < results.length) {
|
|
644
784
|
results = results.slice(0, limit);
|
|
645
785
|
}
|
|
646
|
-
const candidates =
|
|
786
|
+
const candidates = explainLocalEntries.length + explainGlobalEntries.length;
|
|
647
787
|
if (asJson) {
|
|
648
788
|
const output = results.map((r, rank) => ({
|
|
649
789
|
rank: rank + 1,
|
|
@@ -1190,7 +1330,7 @@ function cmdDedup(hippoRoot, flags) {
|
|
|
1190
1330
|
console.log(` ... and ${result.pairs.length - 15} more (run with --dry-run to see all)`);
|
|
1191
1331
|
}
|
|
1192
1332
|
}
|
|
1193
|
-
function cmdSleep(hippoRoot, flags) {
|
|
1333
|
+
async function cmdSleep(hippoRoot, flags) {
|
|
1194
1334
|
// Tee stdout/stderr to a log file when --log-file is set. The SessionEnd
|
|
1195
1335
|
// hook uses this so the output is captured somewhere the SessionStart hook
|
|
1196
1336
|
// can re-display it next time the agent UI starts.
|
|
@@ -1229,7 +1369,7 @@ function cmdSleep(hippoRoot, flags) {
|
|
|
1229
1369
|
}
|
|
1230
1370
|
}
|
|
1231
1371
|
try {
|
|
1232
|
-
cmdSleepCore(hippoRoot, flags);
|
|
1372
|
+
await cmdSleepCore(hippoRoot, flags);
|
|
1233
1373
|
if (logFile)
|
|
1234
1374
|
console.log('[hippo] sleep complete');
|
|
1235
1375
|
}
|
|
@@ -1243,7 +1383,7 @@ function cmdSleep(hippoRoot, flags) {
|
|
|
1243
1383
|
restoreStdout();
|
|
1244
1384
|
}
|
|
1245
1385
|
}
|
|
1246
|
-
function cmdSleepCore(hippoRoot, flags) {
|
|
1386
|
+
async function cmdSleepCore(hippoRoot, flags) {
|
|
1247
1387
|
requireInit(hippoRoot);
|
|
1248
1388
|
// Auto-learn from git before consolidating (unless --no-learn)
|
|
1249
1389
|
if (!flags['no-learn']) {
|
|
@@ -1260,7 +1400,7 @@ function cmdSleepCore(hippoRoot, flags) {
|
|
|
1260
1400
|
}
|
|
1261
1401
|
const dryRun = Boolean(flags['dry-run']);
|
|
1262
1402
|
console.log(`Running consolidation${dryRun ? ' (dry run)' : ''}...`);
|
|
1263
|
-
const result = consolidate(hippoRoot, { dryRun });
|
|
1403
|
+
const result = await consolidate(hippoRoot, { dryRun });
|
|
1264
1404
|
console.log(`\nResults:`);
|
|
1265
1405
|
console.log(` Active memories: ${result.decayed}`);
|
|
1266
1406
|
console.log(` Removed (decayed): ${result.removed}`);
|
|
@@ -2226,8 +2366,11 @@ async function cmdContext(hippoRoot, args, flags) {
|
|
|
2226
2366
|
// When the local store isn't initialized (pinned-only path in a fresh dir),
|
|
2227
2367
|
// skip the local load ā loadAllEntries would auto-create .hippo here and
|
|
2228
2368
|
// we don't want to pollute arbitrary cwds.
|
|
2229
|
-
|
|
2230
|
-
|
|
2369
|
+
let localEntries = hasLocal ? loadAllEntries(hippoRoot) : [];
|
|
2370
|
+
let globalEntries = hasGlobal ? loadAllEntries(globalRoot) : [];
|
|
2371
|
+
// Default context always filters superseded (no --include-superseded / --as-of for context)
|
|
2372
|
+
localEntries = localEntries.filter(e => !e.superseded_by);
|
|
2373
|
+
globalEntries = globalEntries.filter(e => !e.superseded_by);
|
|
2231
2374
|
const allEntries = [...localEntries];
|
|
2232
2375
|
if (allEntries.length === 0 && globalEntries.length === 0)
|
|
2233
2376
|
return; // no memories, zero output
|
|
@@ -3265,6 +3408,44 @@ function cmdWm(hippoRoot, args, flags) {
|
|
|
3265
3408
|
console.error('Usage: hippo wm <push|read|clear|flush>');
|
|
3266
3409
|
process.exit(1);
|
|
3267
3410
|
}
|
|
3411
|
+
function cmdDag(hippoRoot, flags) {
|
|
3412
|
+
requireInit(hippoRoot);
|
|
3413
|
+
const entries = loadAllEntries(hippoRoot);
|
|
3414
|
+
const isStats = flags['stats'] === true;
|
|
3415
|
+
const byLevel = new Map();
|
|
3416
|
+
let unlinked = 0;
|
|
3417
|
+
for (const entry of entries) {
|
|
3418
|
+
const level = entry.dag_level ?? 0;
|
|
3419
|
+
byLevel.set(level, (byLevel.get(level) ?? 0) + 1);
|
|
3420
|
+
if (level === 1 && !entry.dag_parent_id)
|
|
3421
|
+
unlinked++;
|
|
3422
|
+
}
|
|
3423
|
+
if (isStats) {
|
|
3424
|
+
console.log('DAG Structure:');
|
|
3425
|
+
console.log(` Level 3 (entity profiles): ${byLevel.get(3) ?? 0}`);
|
|
3426
|
+
console.log(` Level 2 (topic summaries): ${byLevel.get(2) ?? 0}`);
|
|
3427
|
+
console.log(` Level 1 (extracted facts): ${byLevel.get(1) ?? 0}`);
|
|
3428
|
+
console.log(` Level 0 (raw memories): ${byLevel.get(0) ?? 0}`);
|
|
3429
|
+
console.log(` Unlinked facts: ${unlinked}`);
|
|
3430
|
+
return;
|
|
3431
|
+
}
|
|
3432
|
+
// Tree view: show summaries and their children
|
|
3433
|
+
const summaries = entries.filter((e) => e.dag_level === 2);
|
|
3434
|
+
if (summaries.length === 0) {
|
|
3435
|
+
console.log('No DAG summaries yet. Run `hippo sleep` with ANTHROPIC_API_KEY set.');
|
|
3436
|
+
return;
|
|
3437
|
+
}
|
|
3438
|
+
for (const summary of summaries) {
|
|
3439
|
+
const summaryTags = summary.tags.filter((t) => t !== 'dag-summary').join(', ');
|
|
3440
|
+
console.log(`\nš ${summary.content.slice(0, 80)}`);
|
|
3441
|
+
if (summaryTags)
|
|
3442
|
+
console.log(` [${summaryTags}]`);
|
|
3443
|
+
const children = entries.filter((e) => e.dag_parent_id === summary.id);
|
|
3444
|
+
for (const child of children) {
|
|
3445
|
+
console.log(` āā ${child.content.slice(0, 70)}`);
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3268
3449
|
function printUsage() {
|
|
3269
3450
|
console.log(`
|
|
3270
3451
|
Hippo - biologically-inspired memory system for AI agents
|
|
@@ -3529,7 +3710,7 @@ async function main() {
|
|
|
3529
3710
|
console.error('Memory content too short (minimum 3 characters).');
|
|
3530
3711
|
process.exit(1);
|
|
3531
3712
|
}
|
|
3532
|
-
cmdRemember(hippoRoot, text, flags);
|
|
3713
|
+
await cmdRemember(hippoRoot, text, flags);
|
|
3533
3714
|
break;
|
|
3534
3715
|
}
|
|
3535
3716
|
case 'recall': {
|
|
@@ -3541,6 +3722,16 @@ async function main() {
|
|
|
3541
3722
|
await cmdRecall(hippoRoot, query, flags);
|
|
3542
3723
|
break;
|
|
3543
3724
|
}
|
|
3725
|
+
case 'supersede': {
|
|
3726
|
+
const oldId = args[0];
|
|
3727
|
+
const newContent = args.slice(1).join(' ').trim();
|
|
3728
|
+
if (!oldId || !newContent) {
|
|
3729
|
+
console.error('Usage: hippo supersede <old-id> "<new content>" [--layer L] [--tag T] [--pin]');
|
|
3730
|
+
process.exit(1);
|
|
3731
|
+
}
|
|
3732
|
+
cmdSupersede(hippoRoot, oldId, newContent, flags);
|
|
3733
|
+
break;
|
|
3734
|
+
}
|
|
3544
3735
|
case 'explain': {
|
|
3545
3736
|
const query = args.join(' ').trim();
|
|
3546
3737
|
if (!query) {
|
|
@@ -3572,7 +3763,7 @@ async function main() {
|
|
|
3572
3763
|
await cmdRefine(hippoRoot, flags);
|
|
3573
3764
|
break;
|
|
3574
3765
|
case 'sleep':
|
|
3575
|
-
cmdSleep(hippoRoot, flags);
|
|
3766
|
+
await cmdSleep(hippoRoot, flags);
|
|
3576
3767
|
break;
|
|
3577
3768
|
case 'last-sleep':
|
|
3578
3769
|
cmdLastSleep(flags);
|
|
@@ -3592,6 +3783,9 @@ async function main() {
|
|
|
3592
3783
|
case 'dedup':
|
|
3593
3784
|
cmdDedup(hippoRoot, flags);
|
|
3594
3785
|
break;
|
|
3786
|
+
case 'dag':
|
|
3787
|
+
cmdDag(hippoRoot, flags);
|
|
3788
|
+
break;
|
|
3595
3789
|
case 'audit': {
|
|
3596
3790
|
requireInit(hippoRoot);
|
|
3597
3791
|
const entries = loadAllEntries(hippoRoot);
|