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.
Files changed (49) hide show
  1. package/README.md +16 -0
  2. package/dist/cli.js +213 -19
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.d.ts +7 -0
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +9 -0
  7. package/dist/config.js.map +1 -1
  8. package/dist/consolidate.d.ts +5 -1
  9. package/dist/consolidate.d.ts.map +1 -1
  10. package/dist/consolidate.js +66 -10
  11. package/dist/consolidate.js.map +1 -1
  12. package/dist/dag.d.ts +20 -0
  13. package/dist/dag.d.ts.map +1 -0
  14. package/dist/dag.js +104 -0
  15. package/dist/dag.js.map +1 -0
  16. package/dist/db.d.ts.map +1 -1
  17. package/dist/db.js +36 -1
  18. package/dist/db.js.map +1 -1
  19. package/dist/extract.d.ts +14 -0
  20. package/dist/extract.d.ts.map +1 -0
  21. package/dist/extract.js +87 -0
  22. package/dist/extract.js.map +1 -0
  23. package/dist/index.d.ts +2 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +2 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/memory.d.ts +9 -0
  28. package/dist/memory.d.ts.map +1 -1
  29. package/dist/memory.js +5 -0
  30. package/dist/memory.js.map +1 -1
  31. package/dist/multihop.d.ts +11 -0
  32. package/dist/multihop.d.ts.map +1 -0
  33. package/dist/multihop.js +32 -0
  34. package/dist/multihop.js.map +1 -0
  35. package/dist/search.d.ts +15 -0
  36. package/dist/search.d.ts.map +1 -1
  37. package/dist/search.js +188 -7
  38. package/dist/search.js.map +1 -1
  39. package/dist/shared.d.ts +4 -0
  40. package/dist/shared.d.ts.map +1 -1
  41. package/dist/shared.js +3 -3
  42. package/dist/shared.js.map +1 -1
  43. package/dist/store.d.ts.map +1 -1
  44. package/dist/store.js +23 -3
  45. package/dist/store.js.map +1 -1
  46. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  47. package/extensions/openclaw-plugin/package.json +1 -1
  48. package/openclaw.plugin.json +1 -1
  49. 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
- const localEntries = loadSearchEntries(hippoRoot, query);
451
- const globalEntries = isInitialized(globalRoot) ? loadSearchEntries(globalRoot, query) : [];
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 (usePhysics && !hasGlobal) {
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
- const localEntries = loadSearchEntries(hippoRoot, query);
602
- const globalEntries = isInitialized(globalRoot) ? loadSearchEntries(globalRoot, query) : [];
603
- const hasGlobal = globalEntries.length > 0;
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, localEntries, {
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, localEntries, {
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 = localEntries.length + globalEntries.length;
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
- const localEntries = hasLocal ? loadAllEntries(hippoRoot) : [];
2230
- const globalEntries = hasGlobal ? loadAllEntries(globalRoot) : [];
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);