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.
Files changed (122) hide show
  1. package/README.md +185 -101
  2. package/dist/agent-memories/apply.d.ts +47 -0
  3. package/dist/agent-memories/apply.js +253 -0
  4. package/dist/agent-memories/claude-code.d.ts +11 -0
  5. package/dist/agent-memories/claude-code.js +113 -0
  6. package/dist/agent-memories/codex.d.ts +3 -0
  7. package/dist/agent-memories/codex.js +47 -0
  8. package/dist/agent-memories/copilot.d.ts +3 -0
  9. package/dist/agent-memories/copilot.js +125 -0
  10. package/dist/agent-memories/files.d.ts +37 -0
  11. package/dist/agent-memories/files.js +77 -0
  12. package/dist/agent-memories/folder-store.d.ts +17 -0
  13. package/dist/agent-memories/folder-store.js +44 -0
  14. package/dist/agent-memories/gemini.d.ts +3 -0
  15. package/dist/agent-memories/gemini.js +103 -0
  16. package/dist/agent-memories/git.d.ts +8 -0
  17. package/dist/agent-memories/git.js +11 -0
  18. package/dist/agent-memories/keys.d.ts +9 -0
  19. package/dist/agent-memories/keys.js +20 -0
  20. package/dist/agent-memories/legacy.d.ts +17 -0
  21. package/dist/agent-memories/legacy.js +45 -0
  22. package/dist/agent-memories/markdown.d.ts +13 -0
  23. package/dist/agent-memories/markdown.js +123 -0
  24. package/dist/agent-memories/openclaw.d.ts +3 -0
  25. package/dist/agent-memories/openclaw.js +42 -0
  26. package/dist/agent-memories/plan.d.ts +78 -0
  27. package/dist/agent-memories/plan.js +123 -0
  28. package/dist/agent-memories/qwen-code.d.ts +5 -0
  29. package/dist/agent-memories/qwen-code.js +50 -0
  30. package/dist/agent-memories/report.d.ts +52 -0
  31. package/dist/agent-memories/report.js +88 -0
  32. package/dist/agent-memories/source.d.ts +16 -0
  33. package/dist/agent-memories/source.js +32 -0
  34. package/dist/agent-memories/sync.d.ts +33 -0
  35. package/dist/agent-memories/sync.js +336 -0
  36. package/dist/agent-memories/tools.d.ts +33 -0
  37. package/dist/agent-memories/tools.js +19 -0
  38. package/dist/agent-memories/types.d.ts +42 -0
  39. package/dist/agent-memories/types.js +2 -0
  40. package/dist/api.d.ts +53 -20
  41. package/dist/api.js +141 -97
  42. package/dist/audit.d.ts +2 -1
  43. package/dist/audit.js +68 -2
  44. package/dist/capture.d.ts +48 -22
  45. package/dist/capture.js +186 -161
  46. package/dist/cli.d.ts +0 -2
  47. package/dist/cli.js +750 -797
  48. package/dist/codex-patch.d.ts +12 -0
  49. package/dist/codex-patch.js +71 -0
  50. package/dist/compaction-items.d.ts +18 -0
  51. package/dist/compaction-items.js +60 -0
  52. package/dist/compaction-record.d.ts +94 -0
  53. package/dist/compaction-record.js +546 -0
  54. package/dist/config.d.ts +4 -1
  55. package/dist/config.js +13 -4
  56. package/dist/connectors/slack/types.d.ts +0 -1
  57. package/dist/consolidate.js +87 -34
  58. package/dist/context-render.d.ts +36 -0
  59. package/dist/context-render.js +154 -0
  60. package/dist/dag.js +3 -2
  61. package/dist/db.d.ts +5 -1
  62. package/dist/db.js +46 -14
  63. package/dist/dedupe.d.ts +6 -6
  64. package/dist/dedupe.js +10 -9
  65. package/dist/doctor.d.ts +1 -1
  66. package/dist/doctor.js +69 -4
  67. package/dist/dormant.d.ts +9 -3
  68. package/dist/dormant.js +26 -2
  69. package/dist/embedding-provider.d.ts +2 -1
  70. package/dist/embedding-provider.js +2 -1
  71. package/dist/embeddings.js +23 -3
  72. package/dist/extract.js +5 -1
  73. package/dist/forward-claim-detector.d.ts +1 -1
  74. package/dist/forward-claim-detector.js +1 -1
  75. package/dist/gated-write.d.ts +9 -0
  76. package/dist/gated-write.js +24 -0
  77. package/dist/graph-recall.d.ts +3 -1
  78. package/dist/graph-recall.js +5 -3
  79. package/dist/hooks.d.ts +18 -2
  80. package/dist/hooks.js +128 -32
  81. package/dist/importers.js +5 -12
  82. package/dist/judgment.d.ts +30 -0
  83. package/dist/judgment.js +122 -0
  84. package/dist/mcp/server.js +171 -210
  85. package/dist/memory.d.ts +19 -2
  86. package/dist/memory.js +35 -3
  87. package/dist/merged-row.d.ts +6 -0
  88. package/dist/merged-row.js +35 -0
  89. package/dist/multihop.d.ts +2 -1
  90. package/dist/multihop.js +7 -4
  91. package/dist/physics-state.d.ts +0 -4
  92. package/dist/physics-state.js +0 -6
  93. package/dist/predictions.d.ts +2 -17
  94. package/dist/predictions.js +2 -15
  95. package/dist/reject-flow.d.ts +7 -5
  96. package/dist/reject-flow.js +41 -12
  97. package/dist/salience.js +12 -5
  98. package/dist/same-text.d.ts +17 -0
  99. package/dist/same-text.js +38 -0
  100. package/dist/scheduler.d.ts +4 -0
  101. package/dist/scheduler.js +8 -0
  102. package/dist/search.d.ts +7 -0
  103. package/dist/search.js +16 -32
  104. package/dist/secret-detect.d.ts +2 -0
  105. package/dist/secret-detect.js +6 -0
  106. package/dist/server-detect.js +9 -33
  107. package/dist/server.js +6 -62
  108. package/dist/session-digest.d.ts +79 -0
  109. package/dist/session-digest.js +528 -0
  110. package/dist/shared.d.ts +10 -2
  111. package/dist/shared.js +44 -36
  112. package/dist/store.d.ts +9 -2
  113. package/dist/store.js +25 -2
  114. package/dist/token-ledger.d.ts +46 -8
  115. package/dist/token-ledger.js +140 -21
  116. package/dist/version.d.ts +1 -1
  117. package/dist/version.js +1 -1
  118. package/extensions/openclaw-plugin/README.md +4 -4
  119. package/extensions/openclaw-plugin/openclaw.plugin.json +2 -2
  120. package/extensions/openclaw-plugin/package.json +1 -1
  121. package/openclaw.plugin.json +2 -2
  122. package/package.json +2 -2
package/dist/multihop.js CHANGED
@@ -1,6 +1,9 @@
1
- import { search } from './search.js';
1
+ import { fitBudget, search } from './search.js';
2
2
  export function multihopSearch(query, entries, options = {}) {
3
- const pass1 = search(query, entries, { ...options, budget: (options.budget ?? 4000) * 2 });
3
+ const budget = options.budget ?? 4000;
4
+ // Pass 1 searches wide to find entities, so each return fits the caller's budget, as search() does.
5
+ const fit = (ordered) => fitBudget(ordered, budget, options.minResults ?? 1, options.cost);
6
+ const pass1 = search(query, entries, { ...options, budget: budget * 2 });
4
7
  const topK = pass1.slice(0, 10);
5
8
  if (topK.length === 0)
6
9
  return [];
@@ -17,7 +20,7 @@ export function multihopSearch(query, entries, options = {}) {
17
20
  .map((t) => t.split(':')[1])
18
21
  .filter((e) => !queryLower.includes(e.toLowerCase()));
19
22
  if (newEntities.length === 0)
20
- return pass1;
23
+ return fit(pass1);
21
24
  const followUpQuery = newEntities.join(' ') + ' ' + query;
22
25
  const pass2 = search(followUpQuery, entries, options);
23
26
  const merged = new Map();
@@ -30,6 +33,6 @@ export function multihopSearch(query, entries, options = {}) {
30
33
  // T2 note: PLAIN stable score sort on purpose -- pass1/pass2 inputs are
31
34
  // deterministically ordered (search() carries the content tail), stability
32
35
  // inherits that, and ties keep pass-1 results ahead of pass-2 follow-ups.
33
- return [...merged.values()].sort((a, b) => b.score - a.score);
36
+ return fit([...merged.values()].sort((a, b) => b.score - a.score));
34
37
  }
35
38
  //# sourceMappingURL=multihop.js.map
@@ -26,10 +26,6 @@ export declare function savePhysicsState(db: DatabaseSyncLike, particles: Physic
26
26
  * Returns the new particle (does not persist — caller must save).
27
27
  */
28
28
  export declare function initializeParticle(entry: MemoryEntry, embedding: number[], now?: Date): PhysicsParticle;
29
- /**
30
- * Delete physics state for a memory. (Also handled by CASCADE, but explicit for clarity.)
31
- */
32
- export declare function deletePhysicsState(db: DatabaseSyncLike, memoryId: string): void;
33
29
  /**
34
30
  * Reset all physics states from original embeddings.
35
31
  * Drops existing physics data and re-initializes from the embedding index.
@@ -129,12 +129,6 @@ export function initializeParticle(entry, embedding, now = evalNow()) {
129
129
  lastSimulation: now.toISOString(),
130
130
  };
131
131
  }
132
- /**
133
- * Delete physics state for a memory. (Also handled by CASCADE, but explicit for clarity.)
134
- */
135
- export function deletePhysicsState(db, memoryId) {
136
- db.prepare('DELETE FROM memory_physics WHERE memory_id = ?').run(memoryId);
137
- }
138
132
  /**
139
133
  * Reset all physics states from original embeddings.
140
134
  * Drops existing physics data and re-initializes from the embedding index.
@@ -116,7 +116,7 @@ export interface PredictionBaserate {
116
116
  */
117
117
  export declare function computePredictionBaserate(hippoRoot: string, tenantId: string, classTag: string, actor?: string,
118
118
  /** v0.32 / J3.2 — when false, skip the predict_baserate audit emit. The
119
- * J3.2 orchestrator (computePlanningFallacyHint, below) calls this with
119
+ * J3.2 orchestrator (computePlanningFallacyOutput, below) calls this with
120
120
  * emitAudit=false and emits its own `recall_autodebias_hint` audit row
121
121
  * instead, so the predict_baserate channel stays scoped to deliberate
122
122
  * CLI / HTTP / MCP predict-baserate calls and does NOT pollute on every
@@ -183,10 +183,6 @@ export interface PlanningFallacyWatching {
183
183
  * `computePlanningFallacyOutput`. Carries EITHER `hint` (baserate
184
184
  * available) OR `watching` (regex fired, no baserate), or NEITHER (mode=off,
185
185
  * no queryText, no regex match, or nClosed=0 silent path). Never both.
186
- *
187
- * Existing `computePlanningFallacyHint` (preserved as a backward-compat
188
- * wrapper) returns only the hint variant; new code should call
189
- * `computePlanningFallacyOutput` directly to surface the watching variant.
190
186
  */
191
187
  export interface PlanningFallacyOutput {
192
188
  hint?: PlanningFallacyHint;
@@ -206,8 +202,7 @@ export interface ComputePlanningFallacyHintOpts {
206
202
  actor?: string;
207
203
  }
208
204
  /**
209
- * J3.2 orchestrator (v1.13.4: richer return type — see computePlanningFallacyHint
210
- * below for the backward-compat wrapper that returns only the hint variant).
205
+ * J3.2 orchestrator.
211
206
  *
212
207
  * Composes the forward-claim detector + class resolver + baserate compute,
213
208
  * with telemetry-grade audit emission at every decision point (success,
@@ -233,14 +228,4 @@ export interface ComputePlanningFallacyHintOpts {
233
228
  * on full match+resolve+baserate path. Well under 50ms target.
234
229
  */
235
230
  export declare function computePlanningFallacyOutput(hippoRoot: string, tenantId: string, queryText: string, opts?: ComputePlanningFallacyHintOpts): PlanningFallacyOutput;
236
- /**
237
- * v1.13.4 backward-compat wrapper: thin shim around
238
- * computePlanningFallacyOutput that returns only the hint variant.
239
- * Existing callers (api.recall, cmdRecall, MCP handler) that don't yet
240
- * consume the watching variant continue to work unchanged.
241
- *
242
- * New callers that want to surface the silent no-class-match / tiebreak
243
- * paths to users should call computePlanningFallacyOutput directly.
244
- */
245
- export declare function computePlanningFallacyHint(hippoRoot: string, tenantId: string, queryText: string, opts?: ComputePlanningFallacyHintOpts): PlanningFallacyHint | null;
246
231
  //# sourceMappingURL=predictions.d.ts.map
@@ -284,7 +284,7 @@ export function loadPredictionsByClass(hippoRoot, tenantId, classTag, opts = {})
284
284
  */
285
285
  export function computePredictionBaserate(hippoRoot, tenantId, classTag, actor = 'cli',
286
286
  /** v0.32 / J3.2 — when false, skip the predict_baserate audit emit. The
287
- * J3.2 orchestrator (computePlanningFallacyHint, below) calls this with
287
+ * J3.2 orchestrator (computePlanningFallacyOutput, below) calls this with
288
288
  * emitAudit=false and emits its own `recall_autodebias_hint` audit row
289
289
  * instead, so the predict_baserate channel stays scoped to deliberate
290
290
  * CLI / HTTP / MCP predict-baserate calls and does NOT pollute on every
@@ -487,8 +487,7 @@ function resolveClassFromTokens(hippoRoot, tenantId, queryTokens) {
487
487
  }
488
488
  }
489
489
  /**
490
- * J3.2 orchestrator (v1.13.4: richer return type — see computePlanningFallacyHint
491
- * below for the backward-compat wrapper that returns only the hint variant).
490
+ * J3.2 orchestrator.
492
491
  *
493
492
  * Composes the forward-claim detector + class resolver + baserate compute,
494
493
  * with telemetry-grade audit emission at every decision point (success,
@@ -616,16 +615,4 @@ export function computePlanningFallacyOutput(hippoRoot, tenantId, queryText, opt
616
615
  },
617
616
  };
618
617
  }
619
- /**
620
- * v1.13.4 backward-compat wrapper: thin shim around
621
- * computePlanningFallacyOutput that returns only the hint variant.
622
- * Existing callers (api.recall, cmdRecall, MCP handler) that don't yet
623
- * consume the watching variant continue to work unchanged.
624
- *
625
- * New callers that want to surface the silent no-class-match / tiebreak
626
- * paths to users should call computePlanningFallacyOutput directly.
627
- */
628
- export function computePlanningFallacyHint(hippoRoot, tenantId, queryText, opts = {}) {
629
- return computePlanningFallacyOutput(hippoRoot, tenantId, queryText, opts).hint ?? null;
630
- }
631
618
  //# sourceMappingURL=predictions.js.map
@@ -7,8 +7,8 @@
7
7
  * SAME multi-step transaction + post-commit mirror-purge flow. Extracted
8
8
  * here (leaf module) so neither duplicates it.
9
9
  *
10
- * Module direction: this file imports from store.ts, rejection.ts,
11
- * raw-archive.ts and dormant.ts. Nothing imports FROM this file except
10
+ * Module direction: this file imports from store.ts, rejection.ts, raw-archive.ts,
11
+ * dormant.ts, same-text.ts and merged-row.ts. Nothing imports FROM this file except
12
12
  * cli.ts and api.ts, so it introduces no cycle.
13
13
  */
14
14
  import { type RejectedValueRow } from './rejection.js';
@@ -28,12 +28,14 @@ export interface RejectFlowResult {
28
28
  * tombstone itself stores no content — this is the only place it's seen
29
29
  * again after this call returns). */
30
30
  content: string;
31
- /** Every live row removed this call (all tenant rows whose normalized
32
- * digest matched — not just the id passed, per the K1/R7 duplicate
33
- * lesson). */
31
+ /** Every row removed this call, live or dormant: all whose normalized digest matched (not just the id
32
+ * passed, per the K1/R7 duplicate lesson), and each sleep-merged row holding the value, whose other
33
+ * texts move to a new row: listed in successorIds when it was live, dormantSuccessorIds when dormant. */
34
34
  removedIds: string[];
35
35
  /** Subset of removedIds that were kind='raw' (archived, not deleted). */
36
36
  removedRawIds: string[];
37
+ successorIds: string[];
38
+ dormantSuccessorIds: string[];
37
39
  }
38
40
  /**
39
41
  * `hippo reject` / `api.reject` core flow. ONE connection, one transaction:
@@ -7,15 +7,17 @@
7
7
  * SAME multi-step transaction + post-commit mirror-purge flow. Extracted
8
8
  * here (leaf module) so neither duplicates it.
9
9
  *
10
- * Module direction: this file imports from store.ts, rejection.ts,
11
- * raw-archive.ts and dormant.ts. Nothing imports FROM this file except
10
+ * Module direction: this file imports from store.ts, rejection.ts, raw-archive.ts,
11
+ * dormant.ts, same-text.ts and merged-row.ts. Nothing imports FROM this file except
12
12
  * cli.ts and api.ts, so it introduces no cycle.
13
13
  */
14
14
  import { closeHippoDb } from './db.js';
15
15
  import { appendAuditEvent } from './audit.js';
16
16
  import { archiveRawMemory } from './raw-archive.js';
17
- import { purgeDormantByDigest } from './dormant.js';
18
- import { openStore, deleteEntryCore, purgeMirrorBestEffort, } from './store.js';
17
+ import { deleteDormantRow, listDormantSnapshots, purgeDormantByDigest, replaceDormantEntry } from './dormant.js';
18
+ import { openStore, deleteEntryCore, purgeMirrorBestEffort, selectAllEntries, stampOriginProject, writeEntryDbOnly, writeEntryMirrors, } from './store.js';
19
+ import { heldTexts } from './same-text.js';
20
+ import { mergedSuccessor } from './merged-row.js';
19
21
  import { rejectionDigest, normalizeValueForRejection, insertRejectedValue, deleteRejectedValue, listRejectedValues, } from './rejection.js';
20
22
  /**
21
23
  * `hippo reject` / `api.reject` core flow. ONE connection, one transaction:
@@ -66,6 +68,8 @@ export function rejectValue(opts) {
66
68
  const now = new Date().toISOString();
67
69
  const removedIds = [];
68
70
  const removedRawIds = [];
71
+ const successors = [];
72
+ const dormantSuccessorIds = [];
69
73
  db.exec('BEGIN');
70
74
  try {
71
75
  insertRejectedValue(db, {
@@ -80,13 +84,14 @@ export function rejectValue(opts) {
80
84
  // O(N) scan over the tenant's rows (plan §4): human-triggered command
81
85
  // on ~1-5k-row stores — acceptable, documented. A digest column on
82
86
  // memories is the escape if stores grow 100x; not needed now.
83
- // SAFETY: rows' shape matches the three columns named in the SELECT above.
84
- const rows = db
85
- .prepare(`SELECT id, kind, content FROM memories WHERE tenant_id = ?`)
86
- .all(opts.tenantId);
87
- for (const row of rows) {
88
- if (rejectionDigest(row.content) !== digest)
87
+ const holdsValue = (text) => rejectionDigest(text) === digest;
88
+ const merged = [];
89
+ for (const row of selectAllEntries(db, opts.tenantId)) {
90
+ if (!holdsValue(row.content)) {
91
+ if (heldTexts(row).some(holdsValue))
92
+ merged.push(row);
89
93
  continue;
94
+ }
90
95
  if (row.kind === 'raw') {
91
96
  // Append-only trigger respected — archiveRawMemory is the only
92
97
  // legitimate removal path for kind='raw', and its inner SAVEPOINT
@@ -102,12 +107,34 @@ export function rejectValue(opts) {
102
107
  }
103
108
  removedIds.push(row.id);
104
109
  }
105
- // Dormant copies (src/dormant.ts) go too, in the same transaction: a
110
+ for (const row of merged) {
111
+ const successor = mergedSuccessor(row, holdsValue, new Set(removedIds));
112
+ deleteEntryCore(db, row.id, { actor: opts.actor, suppressForgetAudit: true });
113
+ removedIds.push(row.id);
114
+ if (!successor)
115
+ continue;
116
+ const kept = stampOriginProject(opts.hippoRoot, successor);
117
+ writeEntryDbOnly(db, kept, { actor: opts.actor });
118
+ successors.push(kept);
119
+ }
120
+ // Dormant copies (src/dormant.ts), whole or inside a merged row, go too, in the same transaction: a
106
121
  // rejected value may not linger where `hippo dormant restore` could
107
122
  // bring it back. They have no markdown mirror, so the post-commit
108
123
  // mirror purge below is a no-op for them; they join removedIds for the
109
124
  // audit trail and the caller's report.
110
125
  removedIds.push(...purgeDormantByDigest(db, opts.tenantId, digest));
126
+ for (const dormant of listDormantSnapshots(db, opts.tenantId)) {
127
+ const successor = mergedSuccessor(dormant.entry, holdsValue, new Set(removedIds));
128
+ if (successor === undefined)
129
+ continue;
130
+ removedIds.push(dormant.entry.id);
131
+ if (!successor) {
132
+ deleteDormantRow(db, opts.tenantId, dormant.entry.id);
133
+ continue;
134
+ }
135
+ replaceDormantEntry(db, opts.tenantId, dormant.entry.id, successor);
136
+ dormantSuccessorIds.push(successor.id);
137
+ }
111
138
  try {
112
139
  appendAuditEvent(db, {
113
140
  tenantId: opts.tenantId,
@@ -149,7 +176,9 @@ export function rejectValue(opts) {
149
176
  db.prepare(`UPDATE raw_archive SET mirror_cleaned_at = ? WHERE memory_id = ?`).run(new Date().toISOString(), id);
150
177
  }
151
178
  }
152
- return { digest, content, removedIds, removedRawIds };
179
+ for (const successor of successors)
180
+ writeEntryMirrors(opts.hippoRoot, successor);
181
+ return { digest, content, removedIds, removedRawIds, successorIds: successors.map((s) => s.id), dormantSuccessorIds };
153
182
  }
154
183
  finally {
155
184
  closeHippoDb(db);
package/dist/salience.js CHANGED
@@ -6,6 +6,7 @@
6
6
  * not everything that enters working memory deserves long-term storage.
7
7
  */
8
8
  import { textOverlap } from './search.js';
9
+ import { duplicateKey, heldTextKeys } from './same-text.js';
9
10
  const DEFAULTS = {
10
11
  recentWindow: 20,
11
12
  overlapThreshold: 0.6,
@@ -37,11 +38,17 @@ export function computeSalience(content, tags, recentMemories, options = {}) {
37
38
  }
38
39
  return { decision: 'store', reason: 'error_despite_overlap', score: 0.7 };
39
40
  }
40
- return {
41
- decision: 'skip',
42
- reason: `duplicate (${(duplicateMatch.overlap * 100).toFixed(0)}% overlap with ${duplicateMatch.matchId})`,
43
- score: 0.1,
44
- };
41
+ // A near-duplicate may be a changed value (port 8080 then 8081), so only the same text is skipped.
42
+ const key = duplicateKey(trimmed);
43
+ const same = window.find((m) => heldTextKeys(m).includes(key));
44
+ if (!same) {
45
+ return {
46
+ decision: 'store',
47
+ reason: `near_duplicate (${(duplicateMatch.overlap * 100).toFixed(0)}% overlap with ${duplicateMatch.matchId})`,
48
+ score: 0.5,
49
+ };
50
+ }
51
+ return { decision: 'skip', reason: `duplicate (same text as ${same.id})`, score: 0.1 };
45
52
  }
46
53
  if (isError) {
47
54
  return { decision: 'store', reason: 'error_novel', score: 0.9 };
@@ -0,0 +1,17 @@
1
+ import type { MemoryEntry } from './memory.js';
2
+ type Text = Pick<MemoryEntry, 'content'> & {
3
+ source?: string;
4
+ pinned?: boolean;
5
+ };
6
+ export declare function duplicateKey(text: string): string;
7
+ /** A sleep-merged row's text: a header, a blank line, then each text as a "- " bullet with its later lines indented. */
8
+ export declare function mergedText(header: string, texts: readonly string[]): string;
9
+ /** Each source text a sleep-merged row holds; a two-row merge by an older release kept one text whole after its header. */
10
+ export declare function heldTexts(entry: Text): string[];
11
+ /** Keys of every text a row holds word for word: its own, plus each source text inside a sleep-merged row. */
12
+ export declare function heldTextKeys(entry: Text): string[];
13
+ export declare function storedTextKeys(entries: readonly Text[]): Set<string>;
14
+ /** A final result list without copies: drops each unpinned row a sleep-merged row in the list holds word for word, and each later unpinned copy of a text. */
15
+ export declare function dropHeldCopies<T>(rows: readonly T[], textOf: (row: T) => Text): T[];
16
+ export {};
17
+ //# sourceMappingURL=same-text.d.ts.map
@@ -0,0 +1,38 @@
1
+ export function duplicateKey(text) {
2
+ return text.replace(/\s+/g, ' ').trim();
3
+ }
4
+ /** A sleep-merged row's text: a header, a blank line, then each text as a "- " bullet with its later lines indented. */
5
+ export function mergedText(header, texts) {
6
+ return `${header}\n\n${texts.map((t) => `- ${t.trim().replace(/\n/g, '\n ')}`).join('\n')}`;
7
+ }
8
+ /** Each source text a sleep-merged row holds; a two-row merge by an older release kept one text whole after its header. */
9
+ export function heldTexts(entry) {
10
+ const cut = entry.content.indexOf('\n\n');
11
+ if (entry.source !== 'consolidation' || cut < 0)
12
+ return [];
13
+ const body = entry.content.slice(cut + 2);
14
+ if (/^\[Consolidated from \d+ related memories\]$/.test(entry.content.slice(0, cut)))
15
+ return [body];
16
+ return `\n${body}`.split('\n- ').slice(1).map((t) => t.replace(/\n {2}/g, '\n'));
17
+ }
18
+ /** Keys of every text a row holds word for word: its own, plus each source text inside a sleep-merged row. */
19
+ export function heldTextKeys(entry) {
20
+ return [duplicateKey(entry.content), ...heldTexts(entry).map(duplicateKey)];
21
+ }
22
+ export function storedTextKeys(entries) {
23
+ return new Set(entries.flatMap(heldTextKeys));
24
+ }
25
+ /** A final result list without copies: drops each unpinned row a sleep-merged row in the list holds word for word, and each later unpinned copy of a text. */
26
+ export function dropHeldCopies(rows, textOf) {
27
+ const held = new Set(rows.flatMap((r) => heldTextKeys(textOf(r)).slice(1)));
28
+ const seen = new Set();
29
+ return rows.filter((r) => {
30
+ const keys = heldTextKeys(textOf(r));
31
+ const hidden = seen.has(keys[0]) || (keys.length === 1 && held.has(keys[0]));
32
+ if (hidden && !textOf(r).pinned)
33
+ return false; // rows merged by older releases can hold a pin's text
34
+ seen.add(keys[0]);
35
+ return true;
36
+ });
37
+ }
38
+ //# sourceMappingURL=same-text.js.map
@@ -25,6 +25,10 @@ export declare function saveWorkspaceRegistry(globalRoot: string, registry: Work
25
25
  export declare function registerWorkspace(globalRoot: string, projectDir: string): WorkspaceRegistry;
26
26
  export declare function listRegisteredWorkspaces(globalRoot: string): string[];
27
27
  export declare function buildDailyRunnerCommand(projectDir: string, platform?: NodeJS.Platform): string;
28
+ /** What the Windows task runs. A plain `cmd /c` task opens a visible console every morning. */
29
+ export declare function buildWindowsTaskRun(cmd: string): string;
30
+ /** Argv for `schtasks`, passed without a shell: through cmd.exe the `&&` in /tr split the command. */
31
+ export declare function buildSchtasksCreateArgs(taskName: string, cmd: string): string[];
28
32
  export declare function runDailyMaintenance(workspaces: readonly string[], runCommand: (cwd: string, args: string[]) => void): void;
29
33
  export {};
30
34
  //# sourceMappingURL=scheduler.d.ts.map
package/dist/scheduler.js CHANGED
@@ -65,6 +65,14 @@ export function buildDailyRunnerCommand(projectDir, platform = process.platform)
65
65
  const resolved = path.posix.resolve(projectDir.replace(/\\/g, '/'));
66
66
  return `cd "${resolved}" && hippo daily-runner`;
67
67
  }
68
+ /** What the Windows task runs. A plain `cmd /c` task opens a visible console every morning. */
69
+ export function buildWindowsTaskRun(cmd) {
70
+ return `conhost.exe --headless cmd /c ${cmd}`;
71
+ }
72
+ /** Argv for `schtasks`, passed without a shell: through cmd.exe the `&&` in /tr split the command. */
73
+ export function buildSchtasksCreateArgs(taskName, cmd) {
74
+ return ['/create', '/tn', taskName, '/tr', buildWindowsTaskRun(cmd), '/sc', 'daily', '/st', '06:15', '/f'];
75
+ }
68
76
  export function runDailyMaintenance(workspaces, runCommand) {
69
77
  for (const workspace of workspaces) {
70
78
  const resolved = normalizeWorkspace(workspace);
package/dist/search.d.ts CHANGED
@@ -84,6 +84,9 @@ export interface SearchResult {
84
84
  direction: 'from' | 'to';
85
85
  };
86
86
  }
87
+ /** What a result costs against a budget: the tokens of the text it prints as. */
88
+ export type ResultCost = (r: SearchResult) => number;
89
+ export declare function fitBudget<T extends SearchResult>(ordered: T[], budget: number, minResults: number, cost?: ResultCost): T[];
87
90
  export interface ScoreBreakdown {
88
91
  /**
89
92
  * - `hybrid`: BM25 blended with a non-zero cosine from a cached doc vector.
@@ -186,6 +189,8 @@ export declare function hybridSearch(query: string, entries: MemoryEntry[], opti
186
189
  /** Minimum number of results to return regardless of budget.
187
190
  * Prevents budget saturation when memories are large. Default 1. */
188
191
  minResults?: number;
192
+ /** Budget cost per result; the caller that prints passes the cost of its printed text. */
193
+ cost?: ResultCost;
189
194
  /** Active scope for scope-boost scoring. Auto-detected if not provided. */
190
195
  scope?: string | null;
191
196
  /** Include superseded memories in results. Default false. */
@@ -249,6 +254,7 @@ export declare function physicsSearch(query: string, entries: MemoryEntry[], opt
249
254
  queryEmbedding?: number[];
250
255
  explain?: boolean;
251
256
  minResults?: number;
257
+ cost?: ResultCost;
252
258
  /** Active scope for scope-boost scoring. Auto-detected if not provided. */
253
259
  scope?: string | null;
254
260
  /** Include superseded memories. Default false. Must be threaded through
@@ -277,6 +283,7 @@ export declare function search(query: string, entries: MemoryEntry[], options?:
277
283
  now?: Date;
278
284
  hippoRoot?: string;
279
285
  minResults?: number;
286
+ cost?: ResultCost;
280
287
  includeSuperseded?: boolean;
281
288
  asOf?: string;
282
289
  }): SearchResult[];
package/dist/search.js CHANGED
@@ -138,6 +138,19 @@ export function temporalBoost(entry, direction, range) {
138
138
  return 0.8 + 0.4 * (1 - normalized);
139
139
  }
140
140
  }
141
+ // Skip-and-continue, with the first minResults kept whatever they cost; one loop so every engine spends alike.
142
+ export function fitBudget(ordered, budget, minResults, cost) {
143
+ const results = [];
144
+ let used = 0;
145
+ for (const r of ordered) {
146
+ const tokens = cost ? cost(r) : r.tokens;
147
+ if (results.length >= minResults && used + tokens > budget)
148
+ continue;
149
+ used += tokens;
150
+ results.push(r);
151
+ }
152
+ return results;
153
+ }
141
154
  // ---------------------------------------------------------------------------
142
155
  // v0.30 / E4 — DAG L2 summary scoring helpers
143
156
  // ---------------------------------------------------------------------------
@@ -526,17 +539,7 @@ export async function hybridSearch(query, entries, options = {}) {
526
539
  const withPostRank = reranked.map((r, i) => ({ ...r, postRerankRank: i + 1 }));
527
540
  ordered = [...withPostRank, ...tail];
528
541
  }
529
- // Apply token budget (guarantee at least minResults items)
530
- const results = [];
531
- let usedTokens = 0;
532
- for (let i = 0; i < ordered.length; i++) {
533
- const tokens = ordered[i].tokens;
534
- if (results.length >= minResults && usedTokens + tokens > budget)
535
- continue;
536
- usedTokens += tokens;
537
- results.push(ordered[i]);
538
- }
539
- return results;
542
+ return fitBudget(ordered, budget, minResults, options.cost);
540
543
  }
541
544
  /**
542
545
  * MMR (Maximal Marginal Relevance) re-ranking.
@@ -764,16 +767,7 @@ export async function physicsSearch(query, entries, options = {}) {
764
767
  const merged = mergeScorePools(physicsResults, classicResults);
765
768
  // Sort and apply budget, deterministic tiebreak on ties.
766
769
  merged.sort(compareScoredResults);
767
- const results = [];
768
- let usedTokens = 0;
769
- for (let i = 0; i < merged.length; i++) {
770
- const tokens = merged[i].tokens;
771
- if (results.length >= minResults && usedTokens + tokens > budget)
772
- continue;
773
- usedTokens += tokens;
774
- results.push(merged[i]);
775
- }
776
- return results;
770
+ return fitBudget(merged, budget, minResults, options.cost);
777
771
  }
778
772
  /** Normalize two score pools to [0,1] and combine. */
779
773
  function mergeScorePools(poolA, poolB) {
@@ -906,17 +900,7 @@ export function search(query, entries, options = {}) {
906
900
  }
907
901
  dedupedSync.sort(compareScoredResults);
908
902
  }
909
- // Apply token budget
910
- const results = [];
911
- let usedTokens = 0;
912
- for (let i = 0; i < dedupedSync.length; i++) {
913
- const tokens = dedupedSync[i].tokens;
914
- if (results.length >= minResults && usedTokens + tokens > budget)
915
- continue;
916
- usedTokens += tokens;
917
- results.push(dedupedSync[i]);
918
- }
919
- return results;
903
+ return fitBudget(dedupedSync, budget, minResults, options.cost);
920
904
  }
921
905
  /**
922
906
  * Update retrieval metadata on entries that were returned by a search.
@@ -38,6 +38,8 @@ export declare function detectSecret(entry: {
38
38
  * CS1 pre-compact snapshot fields — can scrub it in place instead.
39
39
  */
40
40
  export declare function redactSecrets(text: string): string;
41
+ /** Memories never hold a raw email address (AGENTS.md); phone numbers are left alone, as their patterns misfire on ids. */
42
+ export declare function maskEmails(text: string): string;
41
43
  /** Stricter redaction for text that leaves the machine: no co-occurrence guard, plus Bearer and Basic auth headers and JWTs. */
42
44
  export declare function redactSecretsStrict(text: string): string;
43
45
  //# sourceMappingURL=secret-detect.d.ts.map
@@ -90,6 +90,12 @@ export function detectSecret(entry) {
90
90
  export function redactSecrets(text) {
91
91
  return redactText(text, false);
92
92
  }
93
+ /** A domain's first label must open on a letter, so asset names like logo@2x.png and pins like react@18.2.0 stay. */
94
+ const EMAIL = /\b[A-Za-z0-9._%+-]+@[A-Za-z][A-Za-z0-9-]*(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}\b/g;
95
+ /** Memories never hold a raw email address (AGENTS.md); phone numbers are left alone, as their patterns misfire on ids. */
96
+ export function maskEmails(text) {
97
+ return text.replace(EMAIL, '[email]');
98
+ }
93
99
  /** Stricter redaction for text that leaves the machine: no co-occurrence guard, plus Bearer and Basic auth headers and JWTs. */
94
100
  export function redactSecretsStrict(text) {
95
101
  return redactText(text, true);
@@ -47,10 +47,7 @@ export async function detectServer(hippoRoot) {
47
47
  info = JSON.parse(readFileSync(path, 'utf8'));
48
48
  }
49
49
  catch {
50
- try {
51
- unlinkSync(path);
52
- }
53
- catch { }
50
+ removePidfile(hippoRoot);
54
51
  return null;
55
52
  }
56
53
  // Probe the process. Sending signal 0 throws if the pid is dead or owned
@@ -61,10 +58,7 @@ export async function detectServer(hippoRoot) {
61
58
  process.kill(info.pid, 0);
62
59
  }
63
60
  catch {
64
- try {
65
- unlinkSync(path);
66
- }
67
- catch { }
61
+ removePidfile(hippoRoot);
68
62
  return null;
69
63
  }
70
64
  // The pid is live, but it may have been reused by an unrelated process, and
@@ -77,19 +71,13 @@ export async function detectServer(hippoRoot) {
77
71
  probeUrl = new URL(info.url);
78
72
  }
79
73
  catch {
80
- try {
81
- unlinkSync(path);
82
- }
83
- catch { }
74
+ removePidfile(hippoRoot);
84
75
  return null;
85
76
  }
86
77
  if (probeUrl.protocol !== 'http:' ||
87
78
  !PIDFILE_LOOPBACK_HOSTS.has(probeUrl.hostname) ||
88
79
  probeUrl.port !== String(info.port)) {
89
- try {
90
- unlinkSync(path);
91
- }
92
- catch { }
80
+ removePidfile(hippoRoot);
93
81
  return null;
94
82
  }
95
83
  // Confirm the process answering on info.url is this hippo server by matching
@@ -103,10 +91,7 @@ export async function detectServer(hippoRoot) {
103
91
  signal: AbortSignal.timeout(HEALTH_PROBE_TIMEOUT_MS),
104
92
  });
105
93
  if (!res.ok || !res.body) {
106
- try {
107
- unlinkSync(path);
108
- }
109
- catch { }
94
+ removePidfile(hippoRoot);
110
95
  return null;
111
96
  }
112
97
  // Read the body under a hard byte cap. The process answering on info.url
@@ -123,10 +108,7 @@ export async function detectServer(hippoRoot) {
123
108
  received += value.byteLength;
124
109
  if (received > HEALTH_BODY_MAX_BYTES) {
125
110
  await reader.cancel();
126
- try {
127
- unlinkSync(path);
128
- }
129
- catch { }
111
+ removePidfile(hippoRoot);
130
112
  return null;
131
113
  }
132
114
  raw += decoder.decode(value, { stream: true });
@@ -134,10 +116,7 @@ export async function detectServer(hippoRoot) {
134
116
  raw += decoder.decode();
135
117
  const body = JSON.parse(raw);
136
118
  if (body.started_at !== info.started_at) {
137
- try {
138
- unlinkSync(path);
139
- }
140
- catch { }
119
+ removePidfile(hippoRoot);
141
120
  return null;
142
121
  }
143
122
  }
@@ -148,10 +127,7 @@ export async function detectServer(hippoRoot) {
148
127
  // SAFETY: err's shape is unknown (catch clause); reading an optional
149
128
  // .name property structurally is safe regardless of the object's actual type.
150
129
  if (err?.name !== 'TimeoutError') {
151
- try {
152
- unlinkSync(path);
153
- }
154
- catch { }
130
+ removePidfile(hippoRoot);
155
131
  }
156
132
  return null;
157
133
  }
@@ -190,7 +166,7 @@ export function removePidfile(hippoRoot) {
190
166
  try {
191
167
  unlinkSync(path);
192
168
  }
193
- catch { }
169
+ catch { /* already gone or undeletable; the next detectServer probe re-checks */ }
194
170
  }
195
171
  /**
196
172
  * Remove the pidfile only if it still describes the caller's own server.