hippo-memory 1.52.8 → 1.53.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 +185 -101
- package/dist/agent-memories/apply.d.ts +47 -0
- package/dist/agent-memories/apply.js +253 -0
- package/dist/agent-memories/claude-code.d.ts +11 -0
- package/dist/agent-memories/claude-code.js +113 -0
- package/dist/agent-memories/codex.d.ts +3 -0
- package/dist/agent-memories/codex.js +47 -0
- package/dist/agent-memories/copilot.d.ts +3 -0
- package/dist/agent-memories/copilot.js +125 -0
- package/dist/agent-memories/files.d.ts +37 -0
- package/dist/agent-memories/files.js +77 -0
- package/dist/agent-memories/folder-store.d.ts +17 -0
- package/dist/agent-memories/folder-store.js +44 -0
- package/dist/agent-memories/gemini.d.ts +3 -0
- package/dist/agent-memories/gemini.js +103 -0
- package/dist/agent-memories/git.d.ts +8 -0
- package/dist/agent-memories/git.js +11 -0
- package/dist/agent-memories/keys.d.ts +9 -0
- package/dist/agent-memories/keys.js +20 -0
- package/dist/agent-memories/legacy.d.ts +17 -0
- package/dist/agent-memories/legacy.js +45 -0
- package/dist/agent-memories/markdown.d.ts +13 -0
- package/dist/agent-memories/markdown.js +123 -0
- package/dist/agent-memories/openclaw.d.ts +3 -0
- package/dist/agent-memories/openclaw.js +42 -0
- package/dist/agent-memories/plan.d.ts +78 -0
- package/dist/agent-memories/plan.js +123 -0
- package/dist/agent-memories/qwen-code.d.ts +5 -0
- package/dist/agent-memories/qwen-code.js +50 -0
- package/dist/agent-memories/report.d.ts +52 -0
- package/dist/agent-memories/report.js +88 -0
- package/dist/agent-memories/source.d.ts +16 -0
- package/dist/agent-memories/source.js +32 -0
- package/dist/agent-memories/sync.d.ts +33 -0
- package/dist/agent-memories/sync.js +336 -0
- package/dist/agent-memories/tools.d.ts +33 -0
- package/dist/agent-memories/tools.js +19 -0
- package/dist/agent-memories/types.d.ts +42 -0
- package/dist/agent-memories/types.js +2 -0
- package/dist/api.d.ts +53 -20
- package/dist/api.js +141 -97
- package/dist/audit.d.ts +2 -1
- package/dist/audit.js +68 -2
- package/dist/capture.d.ts +48 -22
- package/dist/capture.js +186 -161
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +750 -797
- package/dist/codex-patch.d.ts +12 -0
- package/dist/codex-patch.js +71 -0
- package/dist/compaction-items.d.ts +18 -0
- package/dist/compaction-items.js +60 -0
- package/dist/compaction-record.d.ts +94 -0
- package/dist/compaction-record.js +546 -0
- package/dist/config.d.ts +4 -1
- package/dist/config.js +13 -4
- package/dist/connectors/slack/types.d.ts +0 -1
- package/dist/consolidate.js +87 -34
- package/dist/context-render.d.ts +36 -0
- package/dist/context-render.js +154 -0
- package/dist/dag.js +3 -2
- package/dist/db.d.ts +5 -1
- package/dist/db.js +46 -14
- package/dist/dedupe.d.ts +6 -6
- package/dist/dedupe.js +10 -9
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +69 -4
- package/dist/dormant.d.ts +9 -3
- package/dist/dormant.js +26 -2
- package/dist/embedding-provider.d.ts +2 -1
- package/dist/embedding-provider.js +2 -1
- package/dist/embeddings.js +23 -3
- package/dist/extract.js +5 -1
- package/dist/forward-claim-detector.d.ts +1 -1
- package/dist/forward-claim-detector.js +1 -1
- package/dist/gated-write.d.ts +9 -0
- package/dist/gated-write.js +24 -0
- package/dist/graph-recall.d.ts +3 -1
- package/dist/graph-recall.js +5 -3
- package/dist/hooks.d.ts +18 -2
- package/dist/hooks.js +128 -32
- package/dist/importers.js +5 -12
- package/dist/judgment.d.ts +30 -0
- package/dist/judgment.js +122 -0
- package/dist/mcp/server.js +171 -210
- package/dist/memory.d.ts +19 -2
- package/dist/memory.js +35 -3
- package/dist/merged-row.d.ts +6 -0
- package/dist/merged-row.js +35 -0
- package/dist/multihop.d.ts +2 -1
- package/dist/multihop.js +7 -4
- package/dist/physics-state.d.ts +0 -4
- package/dist/physics-state.js +0 -6
- package/dist/predictions.d.ts +2 -17
- package/dist/predictions.js +2 -15
- package/dist/reject-flow.d.ts +7 -5
- package/dist/reject-flow.js +41 -12
- package/dist/salience.js +12 -5
- package/dist/same-text.d.ts +17 -0
- package/dist/same-text.js +38 -0
- package/dist/scheduler.d.ts +4 -0
- package/dist/scheduler.js +8 -0
- package/dist/search.d.ts +7 -0
- package/dist/search.js +16 -32
- package/dist/secret-detect.d.ts +2 -0
- package/dist/secret-detect.js +6 -0
- package/dist/server-detect.js +9 -33
- package/dist/server.js +6 -62
- package/dist/session-digest.d.ts +79 -0
- package/dist/session-digest.js +528 -0
- package/dist/shared.d.ts +10 -2
- package/dist/shared.js +44 -36
- package/dist/store.d.ts +9 -2
- package/dist/store.js +25 -2
- package/dist/token-ledger.d.ts +46 -8
- package/dist/token-ledger.js +140 -21
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/openclaw-plugin/README.md +4 -4
- package/extensions/openclaw-plugin/openclaw.plugin.json +2 -2
- package/extensions/openclaw-plugin/package.json +1 -1
- package/openclaw.plugin.json +2 -2
- package/package.json +2 -2
package/dist/consolidate.js
CHANGED
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
import { evalNow, isRecallBoostAblated } from './ablation.js';
|
|
10
10
|
import { Layer, calculateStrength, canAutoDelete, createMemory } from './memory.js';
|
|
11
11
|
import { loadAllEntries, batchWriteAndDelete, appendConsolidationRun, replaceDetectedConflicts, loadSessionDecayContext, incrementSleepCount, findPromotableSessions, traceExistsForSession, listSessionEvents, } from './store.js';
|
|
12
|
-
import { textOverlap, markRetrieved } from './search.js';
|
|
12
|
+
import { textOverlap, markRetrieved, tokenize } from './search.js';
|
|
13
13
|
import { compareEntryIdentity } from './compare.js';
|
|
14
|
+
import { duplicateKey, mergedText } from './same-text.js';
|
|
15
|
+
import { successorAfterRetirement } from './merged-row.js';
|
|
14
16
|
import { openHippoDb, closeHippoDb } from './db.js';
|
|
15
17
|
import { rejectionDigest, findRejectedValue } from './rejection.js';
|
|
16
18
|
import { countExpiredDormant, purgeExpiredDormant } from './dormant.js';
|
|
@@ -27,9 +29,12 @@ import { appendAuditEvent } from './audit.js';
|
|
|
27
29
|
import { migrateDefaultHalfLife, LEGACY_TYPED_HALF_LIFE } from './half-life-migration.js';
|
|
28
30
|
import { derivationScope, commonDerivationScope, derivationPartitionKey } from './recall-scope.js';
|
|
29
31
|
import { isQuarantineScope } from './quarantine.js';
|
|
32
|
+
import { NO_MERGE_TAGS } from './shared.js';
|
|
30
33
|
const DECAY_THRESHOLD = 0.05;
|
|
31
34
|
const MERGE_OVERLAP_THRESHOLD = 0.35; // Jaccard similarity for "related"
|
|
32
35
|
const MERGE_MIN_CLUSTER = 2; // minimum cluster size to merge
|
|
36
|
+
const MERGE_MAX_SOURCES = 5; // with MERGE_MAX_CHARS, keeps a merged row near 500 tokens, a third of the 1,500-token context budget
|
|
37
|
+
const MERGE_MAX_CHARS = 2000; // total source text; sources past either cap stay unmerged and keep their half-life
|
|
33
38
|
// Half-life scale for merged source episodics. Demotion must go through
|
|
34
39
|
// half_life_days: calculateStrength() recomputes live strength from
|
|
35
40
|
// last_retrieved/half_life and never reads the stored strength field, so a
|
|
@@ -61,6 +66,9 @@ const CONFLICT_STOPWORDS = new Set([
|
|
|
61
66
|
'more', 'most', 'less', 'least', 'other', 'such', 'same', 'new', 'old', 'one', 'two',
|
|
62
67
|
]);
|
|
63
68
|
const REPLAY_COUNT_DEFAULT = 5;
|
|
69
|
+
function keptAsWritten(entry) {
|
|
70
|
+
return entry.tags.some((tag) => NO_MERGE_TAGS.has(tag));
|
|
71
|
+
}
|
|
64
72
|
function isJsonString(value) {
|
|
65
73
|
return typeof value === 'string';
|
|
66
74
|
}
|
|
@@ -145,7 +153,7 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
145
153
|
for (const e of all)
|
|
146
154
|
e.half_life_days = halfLife.halfLives.get(e.id) ?? e.half_life_days;
|
|
147
155
|
const backingObjects = memoriesBackingObjects(hippoRoot);
|
|
148
|
-
// Retirable: auto-deletable (never pinned,
|
|
156
|
+
// Retirable: auto-deletable (never pinned, raw or kept for good) and not backing a first-class object.
|
|
149
157
|
const retirable = (entry) => canAutoDelete(entry) && !backingObjects.has(entry.id);
|
|
150
158
|
const snapshot = new Map(structuredClone(all).map((e) => [e.id, e]));
|
|
151
159
|
// Load decay options from config + session context
|
|
@@ -166,7 +174,7 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
166
174
|
// so it stays where it is (stored strength refreshed) but sits out the
|
|
167
175
|
// rest of this cycle the way a deleted row would. Anything else goes
|
|
168
176
|
// dormant when config.dormant is on, and is deleted otherwise.
|
|
169
|
-
// Only called for rows `retirable` allows (never pinned,
|
|
177
|
+
// Only called for rows `retirable` allows (never pinned, raw, kept for good or backing a first-class object).
|
|
170
178
|
const retireFaded = (entry, strength) => {
|
|
171
179
|
const why = `(strength ${strength.toFixed(4)} < ${DECAY_THRESHOLD})`;
|
|
172
180
|
// A faded secret is deleted, never kept dormant: keeping it would hold a
|
|
@@ -503,7 +511,7 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
503
511
|
// 1.6. Batch extraction — extract facts from episodic memories missing them
|
|
504
512
|
// -------------------------------------------------------------------------
|
|
505
513
|
const extractedFromIds = new Set(survivors.filter((e) => e.extracted_from).map((e) => e.extracted_from));
|
|
506
|
-
const extractionCandidates = survivors.filter((e) => e.layer === Layer.Episodic && !e.superseded_by && !extractedFromIds.has(e.id));
|
|
514
|
+
const extractionCandidates = survivors.filter((e) => e.layer === Layer.Episodic && !e.superseded_by && !extractedFromIds.has(e.id) && !keptAsWritten(e));
|
|
507
515
|
result.extractionCandidates = extractionCandidates.length;
|
|
508
516
|
// extraction.enabled=false is the opt-out for every LLM phase below, key or no key.
|
|
509
517
|
const apiKey = config.extraction.enabled !== false ? (process.env.ANTHROPIC_API_KEY ?? '') : '';
|
|
@@ -674,10 +682,35 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
674
682
|
result.details.push(` ⚠️ physics simulation skipped: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
675
683
|
}
|
|
676
684
|
}
|
|
685
|
+
const byId = new Map(all.map((e) => [e.id, e]));
|
|
686
|
+
const rejectedIn = (tenantId) => (text) => {
|
|
687
|
+
const db = getConsolidateDb();
|
|
688
|
+
return db !== null && findRejectedValue(db, tenantId, rejectionDigest(text)) !== null;
|
|
689
|
+
};
|
|
690
|
+
for (let i = survivors.length - 1; i >= 0; i--) {
|
|
691
|
+
const row = survivors[i];
|
|
692
|
+
const successor = retirable(row) ? successorAfterRetirement(row, byId, rejectedIn(row.tenantId)) : undefined;
|
|
693
|
+
if (successor === undefined)
|
|
694
|
+
continue;
|
|
695
|
+
result.details.push(` ✂️ ${row.id} held a retired text${successor ? `, ${successor.id} holds the rest` : ''}`);
|
|
696
|
+
if (dryRun)
|
|
697
|
+
continue;
|
|
698
|
+
pendingDeletes.push(row.id);
|
|
699
|
+
if (successor) {
|
|
700
|
+
pendingWrites.push(successor);
|
|
701
|
+
survivors[i] = successor;
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
survivors.splice(i, 1);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
677
707
|
// -------------------------------------------------------------------------
|
|
678
708
|
// 3. Merge pass - episodic entries only
|
|
679
709
|
// -------------------------------------------------------------------------
|
|
680
|
-
const
|
|
710
|
+
const alreadyMergedIds = new Set(survivors.flatMap((e) => e.parents));
|
|
711
|
+
const mergeCandidates = survivors.filter((e) => e.layer === Layer.Episodic && !e.superseded_by && !keptAsWritten(e) && !alreadyMergedIds.has(e.id)
|
|
712
|
+
&& !e.pinned // a pin merged with a look-alike would read as one of two values
|
|
713
|
+
&& tokenize(e.content).length > 0);
|
|
681
714
|
const used = new Set();
|
|
682
715
|
// T1 fix (2026-08-15 hardening pass): partition by tenantId BEFORE the
|
|
683
716
|
// overlap loop so a cluster can never span tenants. Previously textOverlap
|
|
@@ -708,17 +741,25 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
708
741
|
const mergeTenant = tenantCandidates[0].tenantId;
|
|
709
742
|
const mergeScope = derivationScope(tenantCandidates[0].scope);
|
|
710
743
|
for (let i = 0; i < tenantCandidates.length; i++) {
|
|
711
|
-
if (used.has(tenantCandidates[i].id))
|
|
744
|
+
if (used.has(tenantCandidates[i].id) || tenantCandidates[i].content.length > MERGE_MAX_CHARS)
|
|
712
745
|
continue;
|
|
713
|
-
const
|
|
746
|
+
const related = [tenantCandidates[i]];
|
|
714
747
|
for (let j = i + 1; j < tenantCandidates.length; j++) {
|
|
715
748
|
if (used.has(tenantCandidates[j].id))
|
|
716
749
|
continue;
|
|
717
750
|
const overlap = textOverlap(tenantCandidates[i].content, tenantCandidates[j].content);
|
|
718
751
|
if (overlap >= MERGE_OVERLAP_THRESHOLD) {
|
|
719
|
-
|
|
752
|
+
related.push(tenantCandidates[j]);
|
|
720
753
|
}
|
|
721
754
|
}
|
|
755
|
+
const cluster = [];
|
|
756
|
+
let clusterChars = 0;
|
|
757
|
+
for (const e of related) {
|
|
758
|
+
if (cluster.length === MERGE_MAX_SOURCES || clusterChars + e.content.length > MERGE_MAX_CHARS)
|
|
759
|
+
continue;
|
|
760
|
+
cluster.push(e);
|
|
761
|
+
clusterChars += e.content.length;
|
|
762
|
+
}
|
|
722
763
|
if (cluster.length < MERGE_MIN_CLUSTER)
|
|
723
764
|
continue;
|
|
724
765
|
// Create a semantic summary
|
|
@@ -734,17 +775,20 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
734
775
|
// always 'default'.
|
|
735
776
|
let semantic = null;
|
|
736
777
|
if (!dryRun) {
|
|
737
|
-
semantic =
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
778
|
+
semantic = {
|
|
779
|
+
...createMemory(mergedContent, {
|
|
780
|
+
layer: Layer.Semantic,
|
|
781
|
+
tags: allTags,
|
|
782
|
+
emotional_valence: maxValence,
|
|
783
|
+
schema_fit: 0.7,
|
|
784
|
+
source: 'consolidation',
|
|
785
|
+
confidence: 'inferred',
|
|
786
|
+
tenantId: mergeTenant,
|
|
787
|
+
scope: mergeScope,
|
|
788
|
+
baseHalfLifeDays: config.defaultHalfLifeDays,
|
|
789
|
+
}),
|
|
790
|
+
parents: cluster.map((e) => e.id),
|
|
791
|
+
};
|
|
748
792
|
}
|
|
749
793
|
// mergeContents is DETERMINISTIC CONCATENATION (not an LLM paraphrase)
|
|
750
794
|
// — if a human rejected exactly this byte-identical rollup before, an
|
|
@@ -756,13 +800,17 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
756
800
|
// the tombstone is lifted.
|
|
757
801
|
const consolidateDb = getConsolidateDb();
|
|
758
802
|
if (consolidateDb && semantic) {
|
|
759
|
-
const
|
|
760
|
-
const
|
|
803
|
+
const newDigest = rejectionDigest(semantic.content);
|
|
804
|
+
const oldDigest = rejectionDigest(legacyMergeContents(related)); // tombstones from older releases hold this format's digest
|
|
805
|
+
const newHit = findRejectedValue(consolidateDb, semantic.tenantId, newDigest);
|
|
806
|
+
const tombstone = newHit ?? findRejectedValue(consolidateDb, semantic.tenantId, oldDigest);
|
|
807
|
+
const mergeDigest = newHit ? newDigest : oldDigest;
|
|
761
808
|
if (tombstone) {
|
|
762
809
|
// Still mark used — these members are not re-tried against a
|
|
763
810
|
// DIFFERENT cluster within this same pass; next sleep re-clusters
|
|
764
811
|
// them fresh.
|
|
765
|
-
|
|
812
|
+
const rejected = newHit ? cluster : related; // the old format digested the uncapped list, so rows past the cap were rejected too
|
|
813
|
+
for (const e of rejected)
|
|
766
814
|
used.add(e.id);
|
|
767
815
|
mergesSkippedRejected++;
|
|
768
816
|
try {
|
|
@@ -773,7 +821,7 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
773
821
|
metadata: {
|
|
774
822
|
digest: mergeDigest,
|
|
775
823
|
reason: tombstone.reason,
|
|
776
|
-
sourceIds:
|
|
824
|
+
sourceIds: rejected.map((e) => e.id),
|
|
777
825
|
},
|
|
778
826
|
});
|
|
779
827
|
}
|
|
@@ -922,17 +970,22 @@ export async function consolidate(hippoRoot, options = {}) {
|
|
|
922
970
|
// Helpers
|
|
923
971
|
// ---------------------------------------------------------------------------
|
|
924
972
|
function mergeContents(entries) {
|
|
925
|
-
//
|
|
926
|
-
//
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
return `[Consolidated from ${entries.length} related memories]\n\n${base}`;
|
|
973
|
+
// Each distinct text goes in once and in full (the merge demotes every source), one bullet with its lines indented, so heldTextKeys can read it back.
|
|
974
|
+
// Newest first says which version is current; compareEntryIdentity settles ties, so the row and its rejection digest depend only on the sources.
|
|
975
|
+
const sorted = [...entries].sort((a, b) => (Date.parse(b.created) - Date.parse(a.created)) || compareEntryIdentity(a, b));
|
|
976
|
+
const texts = new Map();
|
|
977
|
+
for (const e of sorted) {
|
|
978
|
+
if (!texts.has(duplicateKey(e.content)))
|
|
979
|
+
texts.set(duplicateKey(e.content), e.content);
|
|
933
980
|
}
|
|
934
|
-
|
|
935
|
-
|
|
981
|
+
const header = entries.length === 2 ? '[Consolidated from 2 related memories, newest first]' : `[Consolidated pattern from ${entries.length} related memories, newest first]`;
|
|
982
|
+
return mergedText(header, [...texts.values()]);
|
|
983
|
+
}
|
|
984
|
+
function legacyMergeContents(entries) {
|
|
985
|
+
// The old format dropped text, so it is only ever digested to match rejections recorded against it, never written.
|
|
986
|
+
const sorted = [...entries].sort((a, b) => (b.content.length - a.content.length) || compareEntryIdentity(a, b));
|
|
987
|
+
if (entries.length === 2)
|
|
988
|
+
return `[Consolidated from ${entries.length} related memories]\n\n${sorted[0].content}`;
|
|
936
989
|
const bullets = sorted.map((e) => `- ${e.content.split('\n')[0].slice(0, 120)}`).join('\n');
|
|
937
990
|
return `[Consolidated pattern from ${entries.length} related memories]\n\n${bullets}`;
|
|
938
991
|
}
|
|
@@ -966,7 +1019,7 @@ rescuedIds = new Set()) {
|
|
|
966
1019
|
continue;
|
|
967
1020
|
if (survivors[i].superseded_by || survivors[j].superseded_by)
|
|
968
1021
|
continue;
|
|
969
|
-
if (survivors[i].tags.includes('extracted') ||
|
|
1022
|
+
if ([survivors[i], survivors[j]].some((e) => e.tags.includes('extracted') || e.tags.includes('session-digest')))
|
|
970
1023
|
continue;
|
|
971
1024
|
const reasonAndScore = describeConflict(survivors[i], survivors[j]);
|
|
972
1025
|
if (!reasonAndScore)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type MemoryEntry } from './memory.js';
|
|
2
|
+
import { type SessionHandoff } from './handoff.js';
|
|
3
|
+
import type { SessionEvent, TaskSnapshot } from './store.js';
|
|
4
|
+
import type { AssembleCost, AssembleResult, AssembledContextItem, ContextCost, ContextResultEntry, DrillDownChild, DrillDownCost, DrillDownResult } from './api.js';
|
|
5
|
+
export declare function snapshotText(s: TaskSnapshot): string;
|
|
6
|
+
export declare function handoffText(h: SessionHandoff): string;
|
|
7
|
+
/** Needs at least one event; the header reads the latest. */
|
|
8
|
+
export declare function sessionTrailText(events: SessionEvent[]): string;
|
|
9
|
+
export declare function contextHeading(heading: string, entries: number, tokens: number): string;
|
|
10
|
+
export declare function contextLine(item: {
|
|
11
|
+
entry: MemoryEntry;
|
|
12
|
+
isGlobal: boolean;
|
|
13
|
+
}, framing: string, showStrength: boolean, now: Date, strengthPct?: number): string;
|
|
14
|
+
export declare function crossProjectHeading(entries: number): string;
|
|
15
|
+
export declare function crossProjectLine(item: Pick<ContextResultEntry, 'entry' | 'origin'>): string;
|
|
16
|
+
/** A header's token figure is part of the text it counts, so render until the figure matches the text. */
|
|
17
|
+
export declare function settleTokens(render: (tokens: number) => string): string;
|
|
18
|
+
export declare function printedTokens(text: string): number;
|
|
19
|
+
export interface AssembleHeadingCounts {
|
|
20
|
+
sessionId: string;
|
|
21
|
+
items: number;
|
|
22
|
+
tokens: number;
|
|
23
|
+
totalRaw: number;
|
|
24
|
+
summarized: number;
|
|
25
|
+
evicted: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function assembleHeading(c: AssembleHeadingCounts): string;
|
|
28
|
+
export declare function assembleLine(it: AssembledContextItem): string;
|
|
29
|
+
/** The window in full, as the MCP tool returns it; the header counts the whole block. */
|
|
30
|
+
export declare function assembleText(r: AssembleResult): string;
|
|
31
|
+
export declare function assembleCost(sessionId: string): AssembleCost;
|
|
32
|
+
export declare function drillLine(c: DrillDownChild): string;
|
|
33
|
+
export declare function drillText(r: DrillDownResult): string;
|
|
34
|
+
export declare const drillCost: DrillDownCost;
|
|
35
|
+
export declare function contextCost(format: 'markdown' | 'additional-context', framing: string): ContextCost;
|
|
36
|
+
//# sourceMappingURL=context-render.d.ts.map
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// The strings a context block prints. The budget prices these same strings, so selection and print cannot drift.
|
|
2
|
+
import { calculateStrength, confidenceFacets, confidenceLabel } from './memory.js';
|
|
3
|
+
import { evalNow } from './ablation.js';
|
|
4
|
+
import { estimateTokens } from './search.js';
|
|
5
|
+
import { renderAmbientSummary } from './ambient.js';
|
|
6
|
+
import { formatHandoffEvidenceLine } from './handoff.js';
|
|
7
|
+
export function snapshotText(s) {
|
|
8
|
+
return [
|
|
9
|
+
'## Active Task Snapshot\n',
|
|
10
|
+
`- Task: ${s.task}`,
|
|
11
|
+
`- Status: ${s.status}`,
|
|
12
|
+
`- Updated: ${s.updated_at}`,
|
|
13
|
+
`- Source: ${s.source}`,
|
|
14
|
+
...(s.session_id ? [`- Session: ${s.session_id}`] : []),
|
|
15
|
+
'', '### Summary', s.summary, '', '### Next step', s.next_step, '',
|
|
16
|
+
].join('\n');
|
|
17
|
+
}
|
|
18
|
+
export function handoffText(h) {
|
|
19
|
+
const lines = ['## Session Handoff\n', `- Session: ${h.sessionId}`, `- Updated: ${h.updatedAt}`];
|
|
20
|
+
if (h.taskId)
|
|
21
|
+
lines.push(`- Task: ${h.taskId}`);
|
|
22
|
+
if (h.repoRoot)
|
|
23
|
+
lines.push(`- Repo: ${h.repoRoot}`);
|
|
24
|
+
if (h.outcome)
|
|
25
|
+
lines.push(`- Outcome: ${h.outcome}`);
|
|
26
|
+
if (h.targetRuntime)
|
|
27
|
+
lines.push(`- Target runtime: ${h.targetRuntime}`);
|
|
28
|
+
if (h.cardId)
|
|
29
|
+
lines.push(`- Card: ${h.cardId}`);
|
|
30
|
+
lines.push('', '### Summary', h.summary);
|
|
31
|
+
if (h.nextAction)
|
|
32
|
+
lines.push('', '### Next action', h.nextAction);
|
|
33
|
+
if (h.artifacts && h.artifacts.length > 0)
|
|
34
|
+
lines.push('', '### Artifacts', ...h.artifacts.map((a) => `- ${a}`));
|
|
35
|
+
if (h.constraints && h.constraints.length > 0)
|
|
36
|
+
lines.push('', '### Constraints', ...h.constraints.map((c) => `- ${c}`));
|
|
37
|
+
if (h.evidence)
|
|
38
|
+
lines.push('', '### Evidence', formatHandoffEvidenceLine(h.evidence));
|
|
39
|
+
lines.push('');
|
|
40
|
+
return lines.join('\n');
|
|
41
|
+
}
|
|
42
|
+
/** Needs at least one event; the header reads the latest. */
|
|
43
|
+
export function sessionTrailText(events) {
|
|
44
|
+
const latest = events[events.length - 1];
|
|
45
|
+
return [
|
|
46
|
+
'## Recent Session Trail\n',
|
|
47
|
+
`- Session: ${latest.session_id}`,
|
|
48
|
+
`- Task: ${latest.task ?? 'n/a'}`,
|
|
49
|
+
`- Updated: ${latest.created_at}`,
|
|
50
|
+
'',
|
|
51
|
+
...events.map((e) => `- [${e.created_at}] (${e.event_type}) ${e.content}`),
|
|
52
|
+
'',
|
|
53
|
+
].join('\n');
|
|
54
|
+
}
|
|
55
|
+
export function contextHeading(heading, entries, tokens) {
|
|
56
|
+
return `## ${heading} (${entries} entries, ${tokens} tokens)\n`;
|
|
57
|
+
}
|
|
58
|
+
export function contextLine(item, framing, showStrength, now, strengthPct = Math.round(calculateStrength(item.entry) * 100)) {
|
|
59
|
+
const e = item.entry;
|
|
60
|
+
const tagStr = e.tags.length > 0 ? ` [${e.tags.join(', ')}]` : '';
|
|
61
|
+
const strengthStr = showStrength ? ` (${strengthPct}%)` : '';
|
|
62
|
+
const globalPrefix = item.isGlobal ? '[global] ' : '';
|
|
63
|
+
const label = confidenceLabel(e, now);
|
|
64
|
+
const confTag = `[${label.text}]${label.warn ? ' ⚠️' : ''}`;
|
|
65
|
+
if (framing === 'observe') {
|
|
66
|
+
const dateStr = new Date(e.created).toISOString().slice(0, 10);
|
|
67
|
+
// Verified rules print without the date prefix.
|
|
68
|
+
if (confidenceFacets(e, now).tier === 'verified')
|
|
69
|
+
return `- **${confTag} ${globalPrefix}${e.content}**${tagStr}${strengthStr}`;
|
|
70
|
+
return `- **${confTag} Previously observed (${dateStr}): ${globalPrefix}${e.content}**${tagStr}${strengthStr}`;
|
|
71
|
+
}
|
|
72
|
+
if (framing === 'suggest')
|
|
73
|
+
return `- **${confTag} Consider checking: ${globalPrefix}${e.content}**${tagStr}${strengthStr}`;
|
|
74
|
+
return `- **${confTag} ${globalPrefix}${e.content}**${tagStr}${strengthStr}`;
|
|
75
|
+
}
|
|
76
|
+
export function crossProjectHeading(entries) {
|
|
77
|
+
return `\n## Other-project memory (explicitly requested, ${entries} entries)\n`;
|
|
78
|
+
}
|
|
79
|
+
export function crossProjectLine(item) {
|
|
80
|
+
const originLabel = item.origin === null || item.origin === '' ? 'unknown-origin' : item.origin;
|
|
81
|
+
const tagStr = item.entry.tags.length > 0 ? ` [${item.entry.tags.join(', ')}]` : '';
|
|
82
|
+
return `- **[${originLabel}]** ${item.entry.content}${tagStr}`;
|
|
83
|
+
}
|
|
84
|
+
/** A header's token figure is part of the text it counts, so render until the figure matches the text. */
|
|
85
|
+
export function settleTokens(render) {
|
|
86
|
+
let t = 0;
|
|
87
|
+
let text = render(t);
|
|
88
|
+
// The figure only gains digits, so this settles in a few rounds; the cap guards a render that reads the clock.
|
|
89
|
+
for (let i = 0; i < 8 && estimateTokens(text) !== t; i++) {
|
|
90
|
+
t = estimateTokens(text);
|
|
91
|
+
text = render(t);
|
|
92
|
+
}
|
|
93
|
+
return text;
|
|
94
|
+
}
|
|
95
|
+
// Every summary slot at its longest wording, so the footer reserve covers whatever the summary says.
|
|
96
|
+
const WIDEST_AMBIENT = {
|
|
97
|
+
tagEntropy: 1, avgStrength: 0, recencyFreshness: 1, emotionalSkew: -1, schemaFitRatio: 0, errorDensity: 0,
|
|
98
|
+
consolidationRatio: 1, conflictIntensity: 1, extractionCoverage: 1,
|
|
99
|
+
dagDepth: Number.MAX_SAFE_INTEGER, totalMemories: Number.MAX_SAFE_INTEGER,
|
|
100
|
+
};
|
|
101
|
+
// Each piece is priced with the newline that follows it, so the pieces' sum bounds the joined block.
|
|
102
|
+
export function printedTokens(text) {
|
|
103
|
+
return estimateTokens(text + '\n');
|
|
104
|
+
}
|
|
105
|
+
export function assembleHeading(c) {
|
|
106
|
+
return `Session ${c.sessionId} \u2014 ${c.items} items, ${c.tokens} tokens (raw=${c.totalRaw}, summarized=${c.summarized}, evicted=${c.evicted})`;
|
|
107
|
+
}
|
|
108
|
+
export function assembleLine(it) {
|
|
109
|
+
const prefix = it.isSummary ? '[summary]' : it.isFreshTail ? '[tail]' : '[older]';
|
|
110
|
+
return ` ${prefix} ${it.createdAt} ${it.id} - ${it.content}`;
|
|
111
|
+
}
|
|
112
|
+
/** The window in full, as the MCP tool returns it; the header counts the whole block. */
|
|
113
|
+
export function assembleText(r) {
|
|
114
|
+
return settleTokens((t) => [assembleHeading({ ...r, items: r.items.length, tokens: t }), ...r.items.map(assembleLine)].join('\n'));
|
|
115
|
+
}
|
|
116
|
+
// Every format prices the full lines: JSON carries full content, and the CLI's previews are never longer.
|
|
117
|
+
export function assembleCost(sessionId) {
|
|
118
|
+
return {
|
|
119
|
+
item: (it) => printedTokens(assembleLine(it)),
|
|
120
|
+
fixed: (w) => printedTokens(assembleHeading({ sessionId, items: w, tokens: w, totalRaw: w, summarized: w, evicted: w })),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function drillHead(s, shown, total, truncated) {
|
|
124
|
+
const span = s.earliestAt ? ` (${s.earliestAt} -> ${s.latestAt})` : '';
|
|
125
|
+
return `Summary ${s.id} \u2014 ${s.descendantCount} descendants${span}\n ${s.content}\n\nChildren (${shown}/${total}${truncated ? ', truncated' : ''}):`;
|
|
126
|
+
}
|
|
127
|
+
export function drillLine(c) {
|
|
128
|
+
return ` [L${c.dagLevel}] ${c.id} - ${c.content}`;
|
|
129
|
+
}
|
|
130
|
+
export function drillText(r) {
|
|
131
|
+
return [drillHead(r.summary, r.children.length, r.totalChildren, r.truncated), ...r.children.map(drillLine)].join('\n');
|
|
132
|
+
}
|
|
133
|
+
export const drillCost = {
|
|
134
|
+
child: (c) => printedTokens(drillLine(c)),
|
|
135
|
+
fixed: (s, w) => printedTokens(drillHead(s, w, w, true)),
|
|
136
|
+
};
|
|
137
|
+
// Headers and footer are reserved at their widest (counts at the budget itself, strength at 100%) before any entry.
|
|
138
|
+
export function contextCost(format, framing) {
|
|
139
|
+
const hook = format === 'additional-context';
|
|
140
|
+
const price = printedTokens;
|
|
141
|
+
return {
|
|
142
|
+
entry: (item) => price(item.category === 'cross-project' && !(hook && item.promptRecall)
|
|
143
|
+
? crossProjectLine(item)
|
|
144
|
+
: contextLine({ entry: item.entry, isGlobal: item.isGlobal ?? false }, framing, !hook, evalNow(), 100)),
|
|
145
|
+
fixed: (budget, can) => price(contextHeading('Project Memory', budget, budget))
|
|
146
|
+
+ (can.cross ? price(crossProjectHeading(budget)) : 0)
|
|
147
|
+
+ (hook && can.promptRecall ? price(contextHeading('Prompt-Relevant Memory', budget, budget)) : 0)
|
|
148
|
+
+ (!hook && can.ambient ? price(`\n${renderAmbientSummary(WIDEST_AMBIENT)}`) : 0),
|
|
149
|
+
snapshot: (s) => price(snapshotText(s)),
|
|
150
|
+
handoff: (h) => price(handoffText(h)),
|
|
151
|
+
trail: (events) => price(sessionTrailText(events)),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=context-render.js.map
|
package/dist/dag.js
CHANGED
|
@@ -4,6 +4,7 @@ import { RejectedValueError } from './rejection.js';
|
|
|
4
4
|
import { redactSecrets } from './secret-detect.js';
|
|
5
5
|
import { derivationScope, derivationPartitionKey } from './recall-scope.js';
|
|
6
6
|
import { loadConfig } from './config.js';
|
|
7
|
+
import { neverAutoShareTags } from './shared.js';
|
|
7
8
|
export function clusterFacts(facts) {
|
|
8
9
|
if (facts.length === 0)
|
|
9
10
|
return [];
|
|
@@ -120,7 +121,7 @@ export async function buildDag(hippoRoot, facts, opts) {
|
|
|
120
121
|
// (memory.ts:535 defaults tenantId when the option is omitted).
|
|
121
122
|
const summaryEntry = createMemory(summary, {
|
|
122
123
|
layer: Layer.Semantic,
|
|
123
|
-
tags: [...cluster.entityTags, 'dag-summary'],
|
|
124
|
+
tags: [...cluster.entityTags, ...neverAutoShareTags(cluster.members), 'dag-summary'],
|
|
124
125
|
confidence: 'inferred',
|
|
125
126
|
dag_level: 2,
|
|
126
127
|
tenantId: factTenant,
|
|
@@ -329,7 +330,7 @@ export async function buildEntityProfiles(hippoRoot, l2Summaries, opts) {
|
|
|
329
330
|
const nowIso = new Date().toISOString();
|
|
330
331
|
const profileEntry = createMemory(summary, {
|
|
331
332
|
layer: Layer.Semantic,
|
|
332
|
-
tags: [...cluster.entityTags, 'dag-entity-profile'],
|
|
333
|
+
tags: [...cluster.entityTags, ...neverAutoShareTags(cluster.members), 'dag-entity-profile'],
|
|
333
334
|
confidence: 'inferred',
|
|
334
335
|
dag_level: 3,
|
|
335
336
|
tenantId, // HIGH #1 fold: thread tenant explicitly
|
package/dist/db.d.ts
CHANGED
|
@@ -16,7 +16,11 @@ export declare function getCurrentSchemaVersion(): number;
|
|
|
16
16
|
/** Thrown by {@link assertBinaryCompatible}; doctor uses it to pick the upgrade fix over a generic permissions fix. */
|
|
17
17
|
export declare class IncompatibleBinaryError extends Error {
|
|
18
18
|
}
|
|
19
|
-
export declare function
|
|
19
|
+
export declare function isSqliteBusy(error: unknown): boolean;
|
|
20
|
+
/** `busyWaitMs` shortens every lock wait of this open, for a hook that must finish inside its own timeout. */
|
|
21
|
+
export declare function openHippoDb(hippoRoot: string, opts?: {
|
|
22
|
+
busyWaitMs?: number;
|
|
23
|
+
}): DatabaseSyncLike;
|
|
20
24
|
/** Open an existing store without changing it: no mkdir, WAL switch, migration or mirror cleanup. Throws when hippo.db is missing. */
|
|
21
25
|
export declare function openHippoDbReadOnly(hippoRoot: string): DatabaseSyncLike;
|
|
22
26
|
export declare function getSchemaVersion(db: DatabaseSyncLike): number;
|
package/dist/db.js
CHANGED
|
@@ -11,7 +11,7 @@ const require = createRequire(import.meta.url);
|
|
|
11
11
|
// runtime (Node's built-in synchronous SQLite module); there are no bundled
|
|
12
12
|
// types for it here, so this require + cast is the module's documented boundary.
|
|
13
13
|
const { DatabaseSync } = require('node:sqlite');
|
|
14
|
-
const CURRENT_SCHEMA_VERSION =
|
|
14
|
+
const CURRENT_SCHEMA_VERSION = 49;
|
|
15
15
|
const MIGRATIONS = [
|
|
16
16
|
{
|
|
17
17
|
version: 1,
|
|
@@ -2398,12 +2398,12 @@ const MIGRATIONS = [
|
|
|
2398
2398
|
version: 45,
|
|
2399
2399
|
up: (db) => {
|
|
2400
2400
|
// Token ledger (src/token-ledger.ts, ROADMAP TE0): one row per block of
|
|
2401
|
-
// memory text hippo hands an agent (hook, CLI, MCP, HTTP). `event` is
|
|
2402
|
-
//
|
|
2403
|
-
//
|
|
2404
|
-
//
|
|
2405
|
-
// unchanged block. Rows older than the retention
|
|
2406
|
-
// write. Additive only: no min_compatible_binary bump.
|
|
2401
|
+
// memory text hippo hands an agent (hook, CLI, MCP, HTTP). `event` is 'inject'
|
|
2402
|
+
// (sent), 'skip' (unchanged since the session's last inject, not sent), 'reset'
|
|
2403
|
+
// (compaction dropped earlier injections, so the next one must be sent) or
|
|
2404
|
+
// 'reread' (re-read by later calls, booked at session end per call day). block_hash
|
|
2405
|
+
// lets the per-prompt hook skip an unchanged block. Rows older than the retention
|
|
2406
|
+
// window are pruned on write. Additive only: no min_compatible_binary bump.
|
|
2407
2407
|
db.exec(`
|
|
2408
2408
|
CREATE TABLE IF NOT EXISTS token_ledger (
|
|
2409
2409
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -2471,6 +2471,36 @@ const MIGRATIONS = [
|
|
|
2471
2471
|
db.exec(MEMORY_QUARANTINE_DDL);
|
|
2472
2472
|
},
|
|
2473
2473
|
},
|
|
2474
|
+
{
|
|
2475
|
+
version: 49,
|
|
2476
|
+
up: (db) => {
|
|
2477
|
+
// Compaction record (src/compaction-record.ts): one row per Claude Code compaction, written before it (started)
|
|
2478
|
+
// and after it (summarised, done). Not a memory row. Additive only: no min_compatible_binary bump.
|
|
2479
|
+
db.exec(`
|
|
2480
|
+
CREATE TABLE IF NOT EXISTS compactions (
|
|
2481
|
+
tenant_id TEXT NOT NULL DEFAULT 'default',
|
|
2482
|
+
id TEXT NOT NULL,
|
|
2483
|
+
session_id TEXT NOT NULL,
|
|
2484
|
+
origin_project TEXT NOT NULL DEFAULT '',
|
|
2485
|
+
compact_trigger TEXT,
|
|
2486
|
+
cwd TEXT,
|
|
2487
|
+
transcript_path TEXT,
|
|
2488
|
+
snapshot_saved INTEGER NOT NULL DEFAULT 0,
|
|
2489
|
+
started_at TEXT NOT NULL,
|
|
2490
|
+
summarised_at TEXT,
|
|
2491
|
+
summary TEXT,
|
|
2492
|
+
items_json TEXT,
|
|
2493
|
+
items_written INTEGER NOT NULL DEFAULT 0,
|
|
2494
|
+
status TEXT NOT NULL DEFAULT 'started' CHECK (status IN ('started','summarised','done','no-summary')),
|
|
2495
|
+
PRIMARY KEY (tenant_id, id)
|
|
2496
|
+
);
|
|
2497
|
+
CREATE INDEX IF NOT EXISTS idx_compactions_session
|
|
2498
|
+
ON compactions(tenant_id, session_id, started_at);
|
|
2499
|
+
CREATE INDEX IF NOT EXISTS idx_compactions_status
|
|
2500
|
+
ON compactions(tenant_id, status);
|
|
2501
|
+
`);
|
|
2502
|
+
},
|
|
2503
|
+
},
|
|
2474
2504
|
];
|
|
2475
2505
|
function tableHasColumn(db, tableName, columnName) {
|
|
2476
2506
|
if (!/^[a-z_]+$/i.test(tableName))
|
|
@@ -2504,7 +2534,7 @@ function assertBinaryCompatible(db) {
|
|
|
2504
2534
|
`Upgrade hippo-memory to open it; an older binary does not know this schema and could expose private rows or damage the store.`);
|
|
2505
2535
|
}
|
|
2506
2536
|
}
|
|
2507
|
-
function isSqliteBusy(error) {
|
|
2537
|
+
export function isSqliteBusy(error) {
|
|
2508
2538
|
const code = error?.errcode;
|
|
2509
2539
|
return code === 5 || code === 6 || code === 517;
|
|
2510
2540
|
}
|
|
@@ -2526,16 +2556,18 @@ function execWithBusyRetry(db, sql, timeoutMs = 30000) {
|
|
|
2526
2556
|
}
|
|
2527
2557
|
}
|
|
2528
2558
|
}
|
|
2529
|
-
|
|
2559
|
+
/** `busyWaitMs` shortens every lock wait of this open, for a hook that must finish inside its own timeout. */
|
|
2560
|
+
export function openHippoDb(hippoRoot, opts) {
|
|
2530
2561
|
fs.mkdirSync(hippoRoot, { recursive: true });
|
|
2531
2562
|
const db = new DatabaseSync(getHippoDbPath(hippoRoot));
|
|
2563
|
+
const busyWaitMs = opts?.busyWaitMs;
|
|
2532
2564
|
try {
|
|
2533
|
-
db.exec(
|
|
2534
|
-
execWithBusyRetry(db, 'PRAGMA journal_mode = WAL');
|
|
2565
|
+
db.exec(`PRAGMA busy_timeout = ${busyWaitMs ?? 5000}`);
|
|
2566
|
+
execWithBusyRetry(db, 'PRAGMA journal_mode = WAL', busyWaitMs);
|
|
2535
2567
|
db.exec('PRAGMA synchronous = NORMAL');
|
|
2536
2568
|
db.exec('PRAGMA wal_autocheckpoint = 100');
|
|
2537
2569
|
db.exec('PRAGMA foreign_keys = ON');
|
|
2538
|
-
runMigrations(db, hippoRoot);
|
|
2570
|
+
runMigrations(db, hippoRoot, busyWaitMs);
|
|
2539
2571
|
// Path A backfill: delete any orphan markdown mirrors for already-archived
|
|
2540
2572
|
// raw_archive rows. Idempotent via per-row raw_archive.mirror_cleaned_at
|
|
2541
2573
|
// (v21). Wrapped in try/catch — a filesystem failure must not prevent DB open.
|
|
@@ -2576,7 +2608,7 @@ export function openHippoDbReadOnly(hippoRoot) {
|
|
|
2576
2608
|
throw error;
|
|
2577
2609
|
}
|
|
2578
2610
|
}
|
|
2579
|
-
function runMigrations(db, hippoRoot) {
|
|
2611
|
+
function runMigrations(db, hippoRoot, busyWaitMs) {
|
|
2580
2612
|
ensureMetaTable(db);
|
|
2581
2613
|
// Before anything writes, so a stale binary never repairs or migrates a store it does not understand.
|
|
2582
2614
|
assertBinaryCompatible(db);
|
|
@@ -2586,7 +2618,7 @@ function runMigrations(db, hippoRoot) {
|
|
|
2586
2618
|
for (const migration of MIGRATIONS) {
|
|
2587
2619
|
if (migration.version <= currentVersion)
|
|
2588
2620
|
continue;
|
|
2589
|
-
execWithBusyRetry(db, 'BEGIN IMMEDIATE');
|
|
2621
|
+
execWithBusyRetry(db, 'BEGIN IMMEDIATE', busyWaitMs);
|
|
2590
2622
|
try {
|
|
2591
2623
|
// A newer binary may have migrated and raised the minimum while we waited for the lock.
|
|
2592
2624
|
assertBinaryCompatible(db);
|
package/dist/dedupe.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Store-level deduplication. Scans for
|
|
3
|
-
*
|
|
2
|
+
* Store-level deduplication. Scans for memories with the same text apart
|
|
3
|
+
* from spacing, keeps the stronger copy (by strength + retrieval count),
|
|
4
4
|
* removes the rest.
|
|
5
5
|
*
|
|
6
6
|
* Extracted from cli.ts in Episode A (v1.11.3) so `api.sleep` can dedupe
|
|
@@ -59,13 +59,13 @@ export interface DedupResult {
|
|
|
59
59
|
*/
|
|
60
60
|
export declare function strengthBucket(strength: number | null | undefined): number;
|
|
61
61
|
/**
|
|
62
|
-
* Scan the store for
|
|
63
|
-
*
|
|
64
|
-
*
|
|
62
|
+
* Scan the store for duplicates and remove the weaker copy: same text apart
|
|
63
|
+
* from spacing, since a near-duplicate can differ in a value (port, version,
|
|
64
|
+
* path, name), AND the same tenant: the scan is partitioned by
|
|
65
65
|
* tenantId, so byte-identical content in two tenants is never a duplicate
|
|
66
66
|
* pair (the tenant boundary is an isolation boundary; cross-tenant removal
|
|
67
67
|
* was the v1.32.0 known-issue data-loss bug).
|
|
68
|
-
* Keeps the one with higher strength (or more retrievals if tied).
|
|
68
|
+
* Keeps the one with higher strength (or more retrievals if tied). `threshold` is accepted for old callers and ignored.
|
|
69
69
|
*/
|
|
70
70
|
export declare function deduplicateStore(hippoRoot: string, options?: {
|
|
71
71
|
threshold?: number;
|