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 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, { budget });
458
+ results = await searchBothHybrid(query, hippoRoot, globalRoot, {
459
+ budget, mmr: mmrEnabled, mmrLambda,
460
+ });
453
461
  }
454
462
  else {
455
- results = await hybridSearch(query, localEntries, { budget, hippoRoot });
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;