hippo-memory 0.31.0 → 0.32.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 +130 -11
- package/dist/cli.js.map +1 -1
- package/dist/consolidate.js +2 -0
- package/dist/consolidate.js.map +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +14 -1
- package/dist/db.js.map +1 -1
- package/dist/memory.d.ts +3 -0
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +2 -0
- package/dist/memory.js.map +1 -1
- package/dist/search.d.ts +6 -0
- package/dist/search.d.ts.map +1 -1
- package/dist/search.js +48 -0
- 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 +12 -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,14 @@ hippo recall "data pipeline issues" --budget 2000
|
|
|
60
60
|
|
|
61
61
|
---
|
|
62
62
|
|
|
63
|
+
### What's new in v0.32.0
|
|
64
|
+
|
|
65
|
+
- **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.
|
|
66
|
+
- **`--include-superseded`** on `recall` and `explain` surfaces historical memories with a `[superseded]` marker. Useful for "what did I used to think about X?"
|
|
67
|
+
- **`--as-of <ISO-date>`** returns the set of beliefs that were current at a past moment. Invalid dates exit with a clear format hint.
|
|
68
|
+
- **Schema v11, zero breaking changes.** Adds `valid_from` + `superseded_by` columns. Existing v10 stores upgrade on first open, no data loss, no manual migration.
|
|
69
|
+
- **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.
|
|
70
|
+
|
|
63
71
|
### What's new in v0.31.0
|
|
64
72
|
|
|
65
73
|
- **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
|
@@ -438,6 +438,37 @@ function cmdRemember(hippoRoot, text, flags) {
|
|
|
438
438
|
});
|
|
439
439
|
}
|
|
440
440
|
}
|
|
441
|
+
function cmdSupersede(hippoRoot, oldId, newContent, flags) {
|
|
442
|
+
requireInit(hippoRoot);
|
|
443
|
+
const old = readEntry(hippoRoot, oldId);
|
|
444
|
+
if (!old) {
|
|
445
|
+
console.error(`Error: memory ${oldId} not found.`);
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
if (old.superseded_by) {
|
|
449
|
+
console.error(`Error: memory ${oldId} is already superseded by ${old.superseded_by}. Supersede that one instead.`);
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
const layer = (typeof flags['layer'] === 'string' ? flags['layer'] : old.layer);
|
|
453
|
+
const rawTags = flags['tag'];
|
|
454
|
+
const tags = Array.isArray(rawTags)
|
|
455
|
+
? rawTags.map((t) => String(t))
|
|
456
|
+
: typeof rawTags === 'string'
|
|
457
|
+
? rawTags.split(',').map((t) => t.trim()).filter(Boolean)
|
|
458
|
+
: [...old.tags];
|
|
459
|
+
const pinned = flags['pin'] === true || old.pinned;
|
|
460
|
+
const newEntry = createMemory(newContent, {
|
|
461
|
+
layer,
|
|
462
|
+
tags,
|
|
463
|
+
pinned,
|
|
464
|
+
source: old.source,
|
|
465
|
+
confidence: 'verified',
|
|
466
|
+
});
|
|
467
|
+
old.superseded_by = newEntry.id;
|
|
468
|
+
writeEntry(hippoRoot, old);
|
|
469
|
+
writeEntry(hippoRoot, newEntry);
|
|
470
|
+
console.log(`Superseded ${oldId} → ${newEntry.id}`);
|
|
471
|
+
}
|
|
441
472
|
async function cmdRecall(hippoRoot, query, flags) {
|
|
442
473
|
requireInit(hippoRoot);
|
|
443
474
|
const budget = parseInt(String(flags['budget'] ?? '4000'), 10);
|
|
@@ -446,9 +477,43 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
446
477
|
const showWhy = Boolean(flags['why']);
|
|
447
478
|
const forcePhysics = Boolean(flags['physics']);
|
|
448
479
|
const forceClassic = Boolean(flags['classic']);
|
|
480
|
+
const includeSuperseded = Boolean(flags['include-superseded']);
|
|
481
|
+
const asOf = typeof flags['as-of'] === 'string' ? flags['as-of'] : undefined;
|
|
482
|
+
if (asOf !== undefined && Number.isNaN(new Date(asOf).getTime())) {
|
|
483
|
+
console.error(`Error: --as-of value "${asOf}" is not a valid ISO date (e.g. 2026-04-22 or 2026-04-22T12:00:00Z).`);
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
449
486
|
const globalRoot = getGlobalRoot();
|
|
450
|
-
|
|
451
|
-
|
|
487
|
+
let localEntries = loadSearchEntries(hippoRoot, query);
|
|
488
|
+
let globalEntries = isInitialized(globalRoot) ? loadSearchEntries(globalRoot, query) : [];
|
|
489
|
+
// Bi-temporal filtering for physics path (hybridSearch handles it internally)
|
|
490
|
+
if (asOf) {
|
|
491
|
+
const filterAsOf = (entries) => {
|
|
492
|
+
const asOfDate = new Date(asOf);
|
|
493
|
+
const successorValidFrom = new Map();
|
|
494
|
+
for (const e of entries) {
|
|
495
|
+
if (e.superseded_by) {
|
|
496
|
+
const successor = entries.find(s => s.id === e.superseded_by);
|
|
497
|
+
if (successor)
|
|
498
|
+
successorValidFrom.set(e.id, successor.valid_from);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return entries.filter(e => {
|
|
502
|
+
if (new Date(e.valid_from) > asOfDate)
|
|
503
|
+
return false;
|
|
504
|
+
if (!e.superseded_by)
|
|
505
|
+
return true;
|
|
506
|
+
const succVf = successorValidFrom.get(e.id);
|
|
507
|
+
return succVf ? new Date(succVf) > asOfDate : true;
|
|
508
|
+
});
|
|
509
|
+
};
|
|
510
|
+
localEntries = filterAsOf(localEntries);
|
|
511
|
+
globalEntries = filterAsOf(globalEntries);
|
|
512
|
+
}
|
|
513
|
+
else if (!includeSuperseded) {
|
|
514
|
+
localEntries = localEntries.filter(e => !e.superseded_by);
|
|
515
|
+
globalEntries = globalEntries.filter(e => !e.superseded_by);
|
|
516
|
+
}
|
|
452
517
|
const hasGlobal = globalEntries.length > 0;
|
|
453
518
|
// Determine search mode: --physics forces physics, --classic forces BM25+cosine,
|
|
454
519
|
// default uses physics if config.physics.enabled is not false
|
|
@@ -555,6 +620,10 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
555
620
|
if (r.entry.layer === Layer.Trace) {
|
|
556
621
|
base.trace_outcome = r.entry.trace_outcome;
|
|
557
622
|
}
|
|
623
|
+
if (r.entry.superseded_by) {
|
|
624
|
+
base.superseded = true;
|
|
625
|
+
base.superseded_by = r.entry.superseded_by;
|
|
626
|
+
}
|
|
558
627
|
if (showWhy) {
|
|
559
628
|
const explanation = explainMatch(query, r);
|
|
560
629
|
base.confidence = resolveConfidence(r.entry);
|
|
@@ -577,8 +646,9 @@ async function cmdRecall(hippoRoot, query, flags) {
|
|
|
577
646
|
const strengthBar = '\u2588'.repeat(Math.round(e.strength * 10)) + '\u2591'.repeat(10 - Math.round(e.strength * 10));
|
|
578
647
|
const isGlobal = isInitialized(globalRoot) && !localIndex.entries[e.id];
|
|
579
648
|
const globalMark = isGlobal ? ' [global]' : '';
|
|
649
|
+
const supersededMark = e.superseded_by ? ' [superseded]' : '';
|
|
580
650
|
const sourceMark = isGlobal ? ' [global]' : ' [local]';
|
|
581
|
-
console.log(`--- ${e.id} [${e.layer}] ${confLabel}${globalMark} score=${fmt(r.score, 3)} strength=${fmt(e.strength)}`);
|
|
651
|
+
console.log(`--- ${e.id} [${e.layer}] ${confLabel}${globalMark}${supersededMark} score=${fmt(r.score, 3)} strength=${fmt(e.strength)}`);
|
|
582
652
|
console.log(` [${strengthBar}] tags: ${e.tags.join(', ') || 'none'} | retrieved: ${e.retrieval_count}x`);
|
|
583
653
|
if (showWhy) {
|
|
584
654
|
const explanation = explainMatch(query, r);
|
|
@@ -597,10 +667,44 @@ async function cmdExplain(hippoRoot, query, flags) {
|
|
|
597
667
|
const asJson = Boolean(flags['json']);
|
|
598
668
|
const forcePhysics = Boolean(flags['physics']);
|
|
599
669
|
const forceClassic = Boolean(flags['classic']);
|
|
670
|
+
const explainIncludeSuperseded = Boolean(flags['include-superseded']);
|
|
671
|
+
const explainAsOf = typeof flags['as-of'] === 'string' ? flags['as-of'] : undefined;
|
|
672
|
+
if (explainAsOf !== undefined && Number.isNaN(new Date(explainAsOf).getTime())) {
|
|
673
|
+
console.error(`Error: --as-of value "${explainAsOf}" is not a valid ISO date (e.g. 2026-04-22 or 2026-04-22T12:00:00Z).`);
|
|
674
|
+
process.exit(1);
|
|
675
|
+
}
|
|
600
676
|
const globalRoot = getGlobalRoot();
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
677
|
+
let explainLocalEntries = loadSearchEntries(hippoRoot, query);
|
|
678
|
+
let explainGlobalEntries = isInitialized(globalRoot) ? loadSearchEntries(globalRoot, query) : [];
|
|
679
|
+
// Bi-temporal filtering
|
|
680
|
+
if (explainAsOf) {
|
|
681
|
+
const filterAsOfExplain = (entries) => {
|
|
682
|
+
const asOfDate = new Date(explainAsOf);
|
|
683
|
+
const successorValidFrom = new Map();
|
|
684
|
+
for (const e of entries) {
|
|
685
|
+
if (e.superseded_by) {
|
|
686
|
+
const successor = entries.find(s => s.id === e.superseded_by);
|
|
687
|
+
if (successor)
|
|
688
|
+
successorValidFrom.set(e.id, successor.valid_from);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return entries.filter(e => {
|
|
692
|
+
if (new Date(e.valid_from) > asOfDate)
|
|
693
|
+
return false;
|
|
694
|
+
if (!e.superseded_by)
|
|
695
|
+
return true;
|
|
696
|
+
const succVf = successorValidFrom.get(e.id);
|
|
697
|
+
return succVf ? new Date(succVf) > asOfDate : true;
|
|
698
|
+
});
|
|
699
|
+
};
|
|
700
|
+
explainLocalEntries = filterAsOfExplain(explainLocalEntries);
|
|
701
|
+
explainGlobalEntries = filterAsOfExplain(explainGlobalEntries);
|
|
702
|
+
}
|
|
703
|
+
else if (!explainIncludeSuperseded) {
|
|
704
|
+
explainLocalEntries = explainLocalEntries.filter(e => !e.superseded_by);
|
|
705
|
+
explainGlobalEntries = explainGlobalEntries.filter(e => !e.superseded_by);
|
|
706
|
+
}
|
|
707
|
+
const hasGlobal = explainGlobalEntries.length > 0;
|
|
604
708
|
const config = loadConfig(hippoRoot);
|
|
605
709
|
const usePhysics = forcePhysics
|
|
606
710
|
|| (!forceClassic && config.physics.enabled !== false);
|
|
@@ -619,7 +723,7 @@ async function cmdExplain(hippoRoot, query, flags) {
|
|
|
619
723
|
let results;
|
|
620
724
|
let modeUsed;
|
|
621
725
|
if (usePhysics && !hasGlobal) {
|
|
622
|
-
results = await physicsSearch(query,
|
|
726
|
+
results = await physicsSearch(query, explainLocalEntries, {
|
|
623
727
|
budget,
|
|
624
728
|
hippoRoot,
|
|
625
729
|
physicsConfig: config.physics,
|
|
@@ -631,19 +735,21 @@ async function cmdExplain(hippoRoot, query, flags) {
|
|
|
631
735
|
else if (hasGlobal) {
|
|
632
736
|
results = await searchBothHybrid(query, hippoRoot, globalRoot, {
|
|
633
737
|
budget, explain: true, mmr: mmrEnabled, mmrLambda, localBump, scope: explainActiveScope,
|
|
738
|
+
includeSuperseded: explainIncludeSuperseded, asOf: explainAsOf,
|
|
634
739
|
});
|
|
635
740
|
modeUsed = 'searchBothHybrid';
|
|
636
741
|
}
|
|
637
742
|
else {
|
|
638
|
-
results = await hybridSearch(query,
|
|
743
|
+
results = await hybridSearch(query, explainLocalEntries, {
|
|
639
744
|
budget, hippoRoot, explain: true, mmr: mmrEnabled, mmrLambda, scope: explainActiveScope,
|
|
745
|
+
includeSuperseded: explainIncludeSuperseded, asOf: explainAsOf,
|
|
640
746
|
});
|
|
641
747
|
modeUsed = 'hybrid';
|
|
642
748
|
}
|
|
643
749
|
if (limit < results.length) {
|
|
644
750
|
results = results.slice(0, limit);
|
|
645
751
|
}
|
|
646
|
-
const candidates =
|
|
752
|
+
const candidates = explainLocalEntries.length + explainGlobalEntries.length;
|
|
647
753
|
if (asJson) {
|
|
648
754
|
const output = results.map((r, rank) => ({
|
|
649
755
|
rank: rank + 1,
|
|
@@ -2226,8 +2332,11 @@ async function cmdContext(hippoRoot, args, flags) {
|
|
|
2226
2332
|
// When the local store isn't initialized (pinned-only path in a fresh dir),
|
|
2227
2333
|
// skip the local load — loadAllEntries would auto-create .hippo here and
|
|
2228
2334
|
// we don't want to pollute arbitrary cwds.
|
|
2229
|
-
|
|
2230
|
-
|
|
2335
|
+
let localEntries = hasLocal ? loadAllEntries(hippoRoot) : [];
|
|
2336
|
+
let globalEntries = hasGlobal ? loadAllEntries(globalRoot) : [];
|
|
2337
|
+
// Default context always filters superseded (no --include-superseded / --as-of for context)
|
|
2338
|
+
localEntries = localEntries.filter(e => !e.superseded_by);
|
|
2339
|
+
globalEntries = globalEntries.filter(e => !e.superseded_by);
|
|
2231
2340
|
const allEntries = [...localEntries];
|
|
2232
2341
|
if (allEntries.length === 0 && globalEntries.length === 0)
|
|
2233
2342
|
return; // no memories, zero output
|
|
@@ -3541,6 +3650,16 @@ async function main() {
|
|
|
3541
3650
|
await cmdRecall(hippoRoot, query, flags);
|
|
3542
3651
|
break;
|
|
3543
3652
|
}
|
|
3653
|
+
case 'supersede': {
|
|
3654
|
+
const oldId = args[0];
|
|
3655
|
+
const newContent = args.slice(1).join(' ').trim();
|
|
3656
|
+
if (!oldId || !newContent) {
|
|
3657
|
+
console.error('Usage: hippo supersede <old-id> "<new content>" [--layer L] [--tag T] [--pin]');
|
|
3658
|
+
process.exit(1);
|
|
3659
|
+
}
|
|
3660
|
+
cmdSupersede(hippoRoot, oldId, newContent, flags);
|
|
3661
|
+
break;
|
|
3662
|
+
}
|
|
3544
3663
|
case 'explain': {
|
|
3545
3664
|
const query = args.join(' ').trim();
|
|
3546
3665
|
if (!query) {
|