hippo-memory 1.32.0 → 1.32.1

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/dist/dedupe.d.ts CHANGED
@@ -14,7 +14,9 @@
14
14
  * asc). Previously the strength/retrieval-count comparator could tie
15
15
  * exactly with no terminal key, so the survivor fell to load order
16
16
  * (arrival-order-dependent); see `strengthBucket` below for the bucket
17
- * encoding.
17
+ * encoding. As of the tenant-partition fix
18
+ * (docs/plans/2026-08-15-dedupe-tenant-partition.md) the order is scoped
19
+ * WITHIN each tenant group; cross-tenant pairs are never compared.
18
20
  */
19
21
  export interface DedupPair {
20
22
  kept: string;
@@ -51,7 +53,11 @@ export interface DedupPair {
51
53
  export declare function strengthBucket(strength: number | null | undefined): number;
52
54
  /**
53
55
  * Scan the store for near-duplicate memories and remove the weaker copy.
54
- * Two memories are duplicates if their content has > threshold Jaccard overlap.
56
+ * Two memories are duplicates if their content has > threshold Jaccard
57
+ * overlap AND they belong to the same tenant: the scan is partitioned by
58
+ * tenantId, so byte-identical content in two tenants is never a duplicate
59
+ * pair (the tenant boundary is an isolation boundary; cross-tenant removal
60
+ * was the v1.32.0 known-issue data-loss bug).
55
61
  * Keeps the one with higher strength (or more retrievals if tied).
56
62
  */
57
63
  export declare function deduplicateStore(hippoRoot: string, options?: {
@@ -1 +1 @@
1
- {"version":3,"file":"dedupe.d.ts","sourceRoot":"","sources":["../src/dedupe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAMH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAOD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAG1E;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACrD;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,EAAE,CAAA;CAAE,CA6DzC"}
1
+ {"version":3,"file":"dedupe.d.ts","sourceRoot":"","sources":["../src/dedupe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAOH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAOD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAG1E;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACrD;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,SAAS,EAAE,CAAA;CAAE,CA6EzC"}
package/dist/dedupe.js CHANGED
@@ -14,7 +14,9 @@
14
14
  * asc). Previously the strength/retrieval-count comparator could tie
15
15
  * exactly with no terminal key, so the survivor fell to load order
16
16
  * (arrival-order-dependent); see `strengthBucket` below for the bucket
17
- * encoding.
17
+ * encoding. As of the tenant-partition fix
18
+ * (docs/plans/2026-08-15-dedupe-tenant-partition.md) the order is scoped
19
+ * WITHIN each tenant group; cross-tenant pairs are never compared.
18
20
  */
19
21
  import { textOverlap } from './search.js';
20
22
  import { loadAllEntries, deleteEntry } from './store.js';
@@ -50,59 +52,79 @@ export function strengthBucket(strength) {
50
52
  }
51
53
  /**
52
54
  * Scan the store for near-duplicate memories and remove the weaker copy.
53
- * Two memories are duplicates if their content has > threshold Jaccard overlap.
55
+ * Two memories are duplicates if their content has > threshold Jaccard
56
+ * overlap AND they belong to the same tenant: the scan is partitioned by
57
+ * tenantId, so byte-identical content in two tenants is never a duplicate
58
+ * pair (the tenant boundary is an isolation boundary; cross-tenant removal
59
+ * was the v1.32.0 known-issue data-loss bug).
54
60
  * Keeps the one with higher strength (or more retrievals if tied).
55
61
  */
56
62
  export function deduplicateStore(hippoRoot, options = {}) {
57
63
  const threshold = options.threshold ?? 0.7;
58
64
  const dryRun = options.dryRun ?? false;
59
65
  const entries = loadAllEntries(hippoRoot);
60
- // Total order so the survivor is a deterministic function of the entry
61
- // multiset, not of load/ingest order: strength bucket desc
62
- // (materially-stronger survives) -> retrieval_count desc (more-retrieved
63
- // survives on a strength tie) -> compareEntryIdentity (content asc -> id
64
- // asc), the cross-ingest-stable terminal key. Without a terminal key,
65
- // freshly-ingested near-duplicates tie exactly (strength=1,
66
- // retrieval_count=0) and the stable sort falls through to
67
- // loadAllEntries's `created ASC, id ASC` order -- arrival order.
66
+ // Tenant partition (mirrors consolidate.ts mergeCandidatesByTenant and
67
+ // dag.ts unparentedByTenant): group by tenantId BEFORE the sort so a
68
+ // duplicate pair can never form across tenants. Map preserves insertion
69
+ // order, so a single-tenant store (every row 'default') gets exactly one
70
+ // group and the sort plus pair loop below run byte-identical to the
71
+ // pre-fix global pass.
72
+ const entriesByTenant = new Map();
73
+ for (const entry of entries) {
74
+ const bucket = entriesByTenant.get(entry.tenantId);
75
+ if (bucket)
76
+ bucket.push(entry);
77
+ else
78
+ entriesByTenant.set(entry.tenantId, [entry]);
79
+ }
68
80
  // finiteCount mirrors strengthBucket's non-finite hardening on the
69
81
  // retrieval leg: a NaN retrieval_count would make the comparator return
70
82
  // NaN and break the total order the same way a NaN bucket would.
71
83
  // Unreachable via the schema (non-nullable INTEGER column), so this is
72
84
  // symmetry, not a live bug.
73
85
  const finiteCount = (n) => Number.isFinite(n ?? 0) ? (n ?? 0) : 0;
74
- entries.sort((a, b) => {
75
- const bucketDiff = strengthBucket(b.strength) - strengthBucket(a.strength);
76
- if (bucketDiff !== 0)
77
- return bucketDiff;
78
- const retrievalDiff = finiteCount(b.retrieval_count) - finiteCount(a.retrieval_count);
79
- if (retrievalDiff !== 0)
80
- return retrievalDiff;
81
- return compareEntryIdentity(a, b);
82
- });
86
+ // Shared across tenant groups: safe because memory ids are globally
87
+ // unique (crypto.randomUUID at creation), so an id in `removed` can never
88
+ // collide with another tenant's row, and deleteEntry below deletes by
89
+ // primary-key id alone.
83
90
  const removed = new Set();
84
91
  const pairs = [];
85
- for (let i = 0; i < entries.length; i++) {
86
- if (removed.has(entries[i].id))
87
- continue;
88
- for (let j = i + 1; j < entries.length; j++) {
89
- if (removed.has(entries[j].id))
90
- continue;
91
- const similarity = textOverlap(entries[i].content, entries[j].content);
92
- if (similarity <= threshold)
92
+ for (const tenantEntries of entriesByTenant.values()) {
93
+ // The v1.26.3 survivor total order (see the file-level docstring:
94
+ // strength bucket desc -> retrieval_count desc -> compareEntryIdentity),
95
+ // scoped per tenant group: the total order holds within a tenant only,
96
+ // matching the partition above.
97
+ tenantEntries.sort((a, b) => {
98
+ const bucketDiff = strengthBucket(b.strength) - strengthBucket(a.strength);
99
+ if (bucketDiff !== 0)
100
+ return bucketDiff;
101
+ const retrievalDiff = finiteCount(b.retrieval_count) - finiteCount(a.retrieval_count);
102
+ if (retrievalDiff !== 0)
103
+ return retrievalDiff;
104
+ return compareEntryIdentity(a, b);
105
+ });
106
+ for (let i = 0; i < tenantEntries.length; i++) {
107
+ if (removed.has(tenantEntries[i].id))
93
108
  continue;
94
- removed.add(entries[j].id);
95
- pairs.push({
96
- kept: entries[i].id,
97
- keptContent: entries[i].content,
98
- keptLayer: entries[i].layer,
99
- keptStrength: entries[i].strength ?? 0,
100
- removed: entries[j].id,
101
- removedContent: entries[j].content,
102
- removedLayer: entries[j].layer,
103
- removedStrength: entries[j].strength ?? 0,
104
- similarity,
105
- });
109
+ for (let j = i + 1; j < tenantEntries.length; j++) {
110
+ if (removed.has(tenantEntries[j].id))
111
+ continue;
112
+ const similarity = textOverlap(tenantEntries[i].content, tenantEntries[j].content);
113
+ if (similarity <= threshold)
114
+ continue;
115
+ removed.add(tenantEntries[j].id);
116
+ pairs.push({
117
+ kept: tenantEntries[i].id,
118
+ keptContent: tenantEntries[i].content,
119
+ keptLayer: tenantEntries[i].layer,
120
+ keptStrength: tenantEntries[i].strength ?? 0,
121
+ removed: tenantEntries[j].id,
122
+ removedContent: tenantEntries[j].content,
123
+ removedLayer: tenantEntries[j].layer,
124
+ removedStrength: tenantEntries[j].strength ?? 0,
125
+ similarity,
126
+ });
127
+ }
106
128
  }
107
129
  }
108
130
  if (!dryRun) {
@@ -1 +1 @@
1
- {"version":3,"file":"dedupe.js","sourceRoot":"","sources":["../src/dedupe.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dedupe.js","sourceRoot":"","sources":["../src/dedupe.ts"],"names":[],"mappings":"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"}
@@ -14,7 +14,9 @@
14
14
  * asc). Previously the strength/retrieval-count comparator could tie
15
15
  * exactly with no terminal key, so the survivor fell to load order
16
16
  * (arrival-order-dependent); see `strengthBucket` below for the bucket
17
- * encoding.
17
+ * encoding. As of the tenant-partition fix
18
+ * (docs/plans/2026-08-15-dedupe-tenant-partition.md) the order is scoped
19
+ * WITHIN each tenant group; cross-tenant pairs are never compared.
18
20
  */
19
21
  import { textOverlap } from './search.js';
20
22
  import { loadAllEntries, deleteEntry } from './store.js';
@@ -50,59 +52,79 @@ export function strengthBucket(strength) {
50
52
  }
51
53
  /**
52
54
  * Scan the store for near-duplicate memories and remove the weaker copy.
53
- * Two memories are duplicates if their content has > threshold Jaccard overlap.
55
+ * Two memories are duplicates if their content has > threshold Jaccard
56
+ * overlap AND they belong to the same tenant: the scan is partitioned by
57
+ * tenantId, so byte-identical content in two tenants is never a duplicate
58
+ * pair (the tenant boundary is an isolation boundary; cross-tenant removal
59
+ * was the v1.32.0 known-issue data-loss bug).
54
60
  * Keeps the one with higher strength (or more retrievals if tied).
55
61
  */
56
62
  export function deduplicateStore(hippoRoot, options = {}) {
57
63
  const threshold = options.threshold ?? 0.7;
58
64
  const dryRun = options.dryRun ?? false;
59
65
  const entries = loadAllEntries(hippoRoot);
60
- // Total order so the survivor is a deterministic function of the entry
61
- // multiset, not of load/ingest order: strength bucket desc
62
- // (materially-stronger survives) -> retrieval_count desc (more-retrieved
63
- // survives on a strength tie) -> compareEntryIdentity (content asc -> id
64
- // asc), the cross-ingest-stable terminal key. Without a terminal key,
65
- // freshly-ingested near-duplicates tie exactly (strength=1,
66
- // retrieval_count=0) and the stable sort falls through to
67
- // loadAllEntries's `created ASC, id ASC` order -- arrival order.
66
+ // Tenant partition (mirrors consolidate.ts mergeCandidatesByTenant and
67
+ // dag.ts unparentedByTenant): group by tenantId BEFORE the sort so a
68
+ // duplicate pair can never form across tenants. Map preserves insertion
69
+ // order, so a single-tenant store (every row 'default') gets exactly one
70
+ // group and the sort plus pair loop below run byte-identical to the
71
+ // pre-fix global pass.
72
+ const entriesByTenant = new Map();
73
+ for (const entry of entries) {
74
+ const bucket = entriesByTenant.get(entry.tenantId);
75
+ if (bucket)
76
+ bucket.push(entry);
77
+ else
78
+ entriesByTenant.set(entry.tenantId, [entry]);
79
+ }
68
80
  // finiteCount mirrors strengthBucket's non-finite hardening on the
69
81
  // retrieval leg: a NaN retrieval_count would make the comparator return
70
82
  // NaN and break the total order the same way a NaN bucket would.
71
83
  // Unreachable via the schema (non-nullable INTEGER column), so this is
72
84
  // symmetry, not a live bug.
73
85
  const finiteCount = (n) => Number.isFinite(n ?? 0) ? (n ?? 0) : 0;
74
- entries.sort((a, b) => {
75
- const bucketDiff = strengthBucket(b.strength) - strengthBucket(a.strength);
76
- if (bucketDiff !== 0)
77
- return bucketDiff;
78
- const retrievalDiff = finiteCount(b.retrieval_count) - finiteCount(a.retrieval_count);
79
- if (retrievalDiff !== 0)
80
- return retrievalDiff;
81
- return compareEntryIdentity(a, b);
82
- });
86
+ // Shared across tenant groups: safe because memory ids are globally
87
+ // unique (crypto.randomUUID at creation), so an id in `removed` can never
88
+ // collide with another tenant's row, and deleteEntry below deletes by
89
+ // primary-key id alone.
83
90
  const removed = new Set();
84
91
  const pairs = [];
85
- for (let i = 0; i < entries.length; i++) {
86
- if (removed.has(entries[i].id))
87
- continue;
88
- for (let j = i + 1; j < entries.length; j++) {
89
- if (removed.has(entries[j].id))
90
- continue;
91
- const similarity = textOverlap(entries[i].content, entries[j].content);
92
- if (similarity <= threshold)
92
+ for (const tenantEntries of entriesByTenant.values()) {
93
+ // The v1.26.3 survivor total order (see the file-level docstring:
94
+ // strength bucket desc -> retrieval_count desc -> compareEntryIdentity),
95
+ // scoped per tenant group: the total order holds within a tenant only,
96
+ // matching the partition above.
97
+ tenantEntries.sort((a, b) => {
98
+ const bucketDiff = strengthBucket(b.strength) - strengthBucket(a.strength);
99
+ if (bucketDiff !== 0)
100
+ return bucketDiff;
101
+ const retrievalDiff = finiteCount(b.retrieval_count) - finiteCount(a.retrieval_count);
102
+ if (retrievalDiff !== 0)
103
+ return retrievalDiff;
104
+ return compareEntryIdentity(a, b);
105
+ });
106
+ for (let i = 0; i < tenantEntries.length; i++) {
107
+ if (removed.has(tenantEntries[i].id))
93
108
  continue;
94
- removed.add(entries[j].id);
95
- pairs.push({
96
- kept: entries[i].id,
97
- keptContent: entries[i].content,
98
- keptLayer: entries[i].layer,
99
- keptStrength: entries[i].strength ?? 0,
100
- removed: entries[j].id,
101
- removedContent: entries[j].content,
102
- removedLayer: entries[j].layer,
103
- removedStrength: entries[j].strength ?? 0,
104
- similarity,
105
- });
109
+ for (let j = i + 1; j < tenantEntries.length; j++) {
110
+ if (removed.has(tenantEntries[j].id))
111
+ continue;
112
+ const similarity = textOverlap(tenantEntries[i].content, tenantEntries[j].content);
113
+ if (similarity <= threshold)
114
+ continue;
115
+ removed.add(tenantEntries[j].id);
116
+ pairs.push({
117
+ kept: tenantEntries[i].id,
118
+ keptContent: tenantEntries[i].content,
119
+ keptLayer: tenantEntries[i].layer,
120
+ keptStrength: tenantEntries[i].strength ?? 0,
121
+ removed: tenantEntries[j].id,
122
+ removedContent: tenantEntries[j].content,
123
+ removedLayer: tenantEntries[j].layer,
124
+ removedStrength: tenantEntries[j].strength ?? 0,
125
+ similarity,
126
+ });
127
+ }
106
128
  }
107
129
  }
108
130
  if (!dryRun) {
@@ -1 +1 @@
1
- {"version":3,"file":"dedupe.js","sourceRoot":"","sources":["../../src/dedupe.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dedupe.js","sourceRoot":"","sources":["../../src/dedupe.ts"],"names":[],"mappings":"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"}
@@ -18,7 +18,7 @@
18
18
  * an ESM `import` can resolve cleanly, and a hardcoded constant survives
19
19
  * any packager that drops .json files.
20
20
  */
21
- export const PACKAGE_VERSION = '1.32.0';
21
+ export const PACKAGE_VERSION = '1.32.1';
22
22
  // Bump on every release alongside the 4 other manifests + lockfile.
23
23
  /**
24
24
  * Compare two semver strings. Returns positive if a > b, 0 if equal, negative
package/dist/version.d.ts CHANGED
@@ -18,7 +18,7 @@
18
18
  * an ESM `import` can resolve cleanly, and a hardcoded constant survives
19
19
  * any packager that drops .json files.
20
20
  */
21
- export declare const PACKAGE_VERSION = "1.32.0";
21
+ export declare const PACKAGE_VERSION = "1.32.1";
22
22
  /**
23
23
  * Compare two semver strings. Returns positive if a > b, 0 if equal, negative
24
24
  * if a < b.
package/dist/version.js CHANGED
@@ -18,7 +18,7 @@
18
18
  * an ESM `import` can resolve cleanly, and a hardcoded constant survives
19
19
  * any packager that drops .json files.
20
20
  */
21
- export const PACKAGE_VERSION = '1.32.0';
21
+ export const PACKAGE_VERSION = '1.32.1';
22
22
  // Bump on every release alongside the 4 other manifests + lockfile.
23
23
  /**
24
24
  * Compare two semver strings. Returns positive if a > b, 0 if equal, negative
@@ -2,7 +2,7 @@
2
2
  "id": "hippo-memory",
3
3
  "name": "Hippo Memory",
4
4
  "description": "Biologically-inspired memory for AI agents. Decay by default, retrieval strengthening, sleep consolidation.",
5
- "version": "1.32.0",
5
+ "version": "1.32.1",
6
6
 
7
7
  "configSchema": {
8
8
  "type": "object",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hippo-memory",
3
- "version": "1.32.0",
3
+ "version": "1.32.1",
4
4
  "description": "Hippo Memory plugin for OpenClaw - biologically-inspired agent memory",
5
5
  "main": "index.ts",
6
6
  "openclaw": {
@@ -2,7 +2,7 @@
2
2
  "id": "hippo-memory",
3
3
  "name": "Hippo Memory",
4
4
  "description": "Biologically-inspired memory for AI agents. Decay by default, retrieval strengthening, sleep consolidation.",
5
- "version": "1.32.0",
5
+ "version": "1.32.1",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hippo-memory",
3
- "version": "1.32.0",
3
+ "version": "1.32.1",
4
4
  "description": "Biologically-inspired memory system for AI agents. Decay by default, strength through use.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",