hippo-memory 1.63.2 → 1.64.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.
Files changed (280) hide show
  1. package/README.md +11 -5
  2. package/dist/agent-memories/claude-code.js +3 -3
  3. package/dist/ambient-store.d.ts +2 -0
  4. package/dist/ambient-store.js +4 -3
  5. package/dist/api/assemble.d.ts +2 -0
  6. package/dist/api/assemble.js +11 -7
  7. package/dist/api/auth.d.ts +15 -11
  8. package/dist/api/auth.js +116 -22
  9. package/dist/api/context-select.d.ts +5 -3
  10. package/dist/api/context-select.js +6 -11
  11. package/dist/api/context-types.d.ts +2 -1
  12. package/dist/api/context.d.ts +1 -1
  13. package/dist/api/context.js +79 -55
  14. package/dist/api/dormant.d.ts +5 -5
  15. package/dist/api/dormant.js +13 -9
  16. package/dist/api/drill-down.d.ts +2 -0
  17. package/dist/api/drill-down.js +12 -7
  18. package/dist/api/forget.d.ts +1 -1
  19. package/dist/api/forget.js +8 -4
  20. package/dist/api/learn.js +5 -3
  21. package/dist/api/outcome.d.ts +1 -1
  22. package/dist/api/outcome.js +2 -1
  23. package/dist/api/promote.js +8 -5
  24. package/dist/api/recall-types.d.ts +4 -2
  25. package/dist/api/recall.js +51 -36
  26. package/dist/api/remember.d.ts +7 -0
  27. package/dist/api/remember.js +26 -2
  28. package/dist/api/sleep-run.d.ts +28 -0
  29. package/dist/api/sleep-run.js +267 -0
  30. package/dist/api/sleep.d.ts +5 -45
  31. package/dist/api/sleep.js +5 -252
  32. package/dist/api/types.d.ts +5 -0
  33. package/dist/api/types.js +4 -0
  34. package/dist/audit-prune.js +1 -1
  35. package/dist/audit.d.ts +1 -3
  36. package/dist/audit.js +7 -103
  37. package/dist/auth.d.ts +48 -4
  38. package/dist/auth.js +111 -42
  39. package/dist/autolearn.js +2 -2
  40. package/dist/capture/caller-session.d.ts +16 -0
  41. package/dist/capture/caller-session.js +58 -0
  42. package/dist/capture/command.d.ts +19 -1
  43. package/dist/capture/command.js +28 -21
  44. package/dist/capture/compact-resume-caller.d.ts +11 -0
  45. package/dist/capture/compact-resume-caller.js +35 -0
  46. package/dist/capture/compact.d.ts +0 -14
  47. package/dist/capture/compact.js +7 -160
  48. package/dist/capture/compaction-items-caller.d.ts +16 -0
  49. package/dist/capture/compaction-items-caller.js +32 -0
  50. package/dist/capture/extract.d.ts +2 -14
  51. package/dist/capture/extract.js +135 -346
  52. package/dist/capture/failure-caller.d.ts +21 -0
  53. package/dist/capture/failure-caller.js +74 -0
  54. package/dist/capture/failure-reading.d.ts +44 -0
  55. package/dist/capture/failure-reading.js +78 -0
  56. package/dist/capture/pre-compact-caller.d.ts +16 -0
  57. package/dist/capture/pre-compact-caller.js +39 -0
  58. package/dist/capture/session-end-caller.d.ts +21 -0
  59. package/dist/capture/session-end-caller.js +30 -0
  60. package/dist/capture/session-texts.d.ts +14 -0
  61. package/dist/capture/session-texts.js +42 -0
  62. package/dist/capture/transcript.d.ts +6 -1
  63. package/dist/capture/transcript.js +20 -16
  64. package/dist/capture/working-state.d.ts +21 -0
  65. package/dist/capture/working-state.js +154 -0
  66. package/dist/capture-error.d.ts +8 -22
  67. package/dist/capture-error.js +9 -61
  68. package/dist/cli/audit.js +25 -0
  69. package/dist/cli/auth.js +4 -3
  70. package/dist/cli/context.js +13 -158
  71. package/dist/cli/continuity.js +2 -1
  72. package/dist/cli/init.js +5 -3
  73. package/dist/cli/last-sleep.d.ts +6 -0
  74. package/dist/cli/last-sleep.js +68 -0
  75. package/dist/cli/maintenance.d.ts +2 -1
  76. package/dist/cli/maintenance.js +17 -11
  77. package/dist/cli/quality-repair-once.d.ts +3 -0
  78. package/dist/cli/quality-repair-once.js +36 -0
  79. package/dist/cli/recall.js +2 -1
  80. package/dist/cli/remember.d.ts +2 -0
  81. package/dist/cli/remember.js +4 -1
  82. package/dist/cli/session-hooks.d.ts +0 -3
  83. package/dist/cli/session-hooks.js +12 -49
  84. package/dist/cli/setup.js +10 -2
  85. package/dist/cli/shared.d.ts +7 -14
  86. package/dist/cli/shared.js +19 -78
  87. package/dist/cli/sleep.js +6 -3
  88. package/dist/cli/usage.js +18 -9
  89. package/dist/cli.d.ts +1 -1
  90. package/dist/cli.js +3 -2
  91. package/dist/client.js +1 -1
  92. package/dist/compaction-record.d.ts +19 -1
  93. package/dist/compaction-record.js +61 -132
  94. package/dist/compaction-spool.d.ts +45 -0
  95. package/dist/compaction-spool.js +454 -0
  96. package/dist/config.d.ts +8 -0
  97. package/dist/config.js +49 -0
  98. package/dist/connectors/github/deletion.d.ts +1 -1
  99. package/dist/connectors/github/deletion.js +5 -17
  100. package/dist/consolidate/conflicts.js +4 -0
  101. package/dist/consolidate/flush-units.d.ts +9 -0
  102. package/dist/consolidate/flush-units.js +63 -0
  103. package/dist/consolidate/llm-passes.js +5 -4
  104. package/dist/consolidate/merge.js +5 -1
  105. package/dist/consolidate/run.d.ts +2 -1
  106. package/dist/consolidate/sleep.d.ts +2 -0
  107. package/dist/consolidate/sleep.js +82 -15
  108. package/dist/context-render.d.ts +14 -0
  109. package/dist/context-render.js +28 -0
  110. package/dist/dag.js +6 -0
  111. package/dist/dashboard-actions.js +3 -2
  112. package/dist/dashboard.js +4 -3
  113. package/dist/db/backup.d.ts +6 -0
  114. package/dist/db/backup.js +29 -0
  115. package/dist/db/busy.d.ts +9 -0
  116. package/dist/db/busy.js +35 -0
  117. package/dist/db/continuity.d.ts +1 -0
  118. package/dist/db/continuity.js +31 -4
  119. package/dist/db/meta.d.ts +2 -0
  120. package/dist/db/meta.js +7 -0
  121. package/dist/db/migrate.js +2 -3
  122. package/dist/db/migrations/index.d.ts +1 -1
  123. package/dist/db/migrations/index.js +5 -1
  124. package/dist/db/migrations/v39.js +2 -7
  125. package/dist/db/migrations/v53.d.ts +3 -0
  126. package/dist/db/migrations/v53.js +16 -0
  127. package/dist/db/migrations/v54.d.ts +3 -0
  128. package/dist/db/migrations/v54.js +25 -0
  129. package/dist/db/open.d.ts +11 -0
  130. package/dist/db/open.js +27 -0
  131. package/dist/db/tables.d.ts +2 -0
  132. package/dist/db/tables.js +6 -2
  133. package/dist/db.d.ts +2 -2
  134. package/dist/db.js +2 -2
  135. package/dist/doctor.d.ts +1 -1
  136. package/dist/doctor.js +31 -15
  137. package/dist/dormant.d.ts +16 -20
  138. package/dist/dormant.js +24 -30
  139. package/dist/embedding-provider.js +4 -3
  140. package/dist/entry/json-hooks.d.ts +3 -0
  141. package/dist/entry/json-hooks.js +4 -0
  142. package/dist/entry/project-identity.d.ts +2 -0
  143. package/dist/entry/project-identity.js +3 -0
  144. package/dist/entry/session-text.d.ts +7 -0
  145. package/dist/entry/session-text.js +8 -0
  146. package/dist/extract.js +6 -0
  147. package/dist/failure-log.d.ts +10 -2
  148. package/dist/failure-log.js +19 -2
  149. package/dist/gated-write.js +1 -1
  150. package/dist/graph/delta.d.ts +44 -0
  151. package/dist/graph/delta.js +109 -0
  152. package/dist/graph/types.d.ts +5 -0
  153. package/dist/graph/write.d.ts +20 -20
  154. package/dist/graph/write.js +103 -36
  155. package/dist/graph-extract.d.ts +43 -9
  156. package/dist/graph-extract.js +140 -157
  157. package/dist/graph-recall.d.ts +1 -0
  158. package/dist/graph-recall.js +2 -2
  159. package/dist/graph-view.d.ts +1 -0
  160. package/dist/graph-view.js +25 -2
  161. package/dist/half-life-migration.js +5 -1
  162. package/dist/handoff-evidence.d.ts +5 -0
  163. package/dist/handoff-evidence.js +30 -0
  164. package/dist/home-path.d.ts +5 -0
  165. package/dist/home-path.js +24 -0
  166. package/dist/hooks/json-hooks.d.ts +13 -2
  167. package/dist/hooks/json-hooks.js +246 -117
  168. package/dist/hooks/opencode.js +1 -1
  169. package/dist/hooks/shared.d.ts +2 -0
  170. package/dist/hooks/shared.js +6 -1
  171. package/dist/http-util.d.ts +16 -7
  172. package/dist/http-util.js +65 -20
  173. package/dist/importers/vault.js +2 -0
  174. package/dist/index.d.ts +2 -1
  175. package/dist/index.js +1 -0
  176. package/dist/json.d.ts +2 -0
  177. package/dist/json.js +7 -0
  178. package/dist/ledger-db.d.ts +10 -0
  179. package/dist/ledger-db.js +42 -0
  180. package/dist/local-embedding.d.ts +2 -0
  181. package/dist/local-embedding.js +20 -4
  182. package/dist/mcp/admin-tools.d.ts +2 -2
  183. package/dist/mcp/admin-tools.js +47 -5
  184. package/dist/mcp/memory-tools.js +7 -2
  185. package/dist/mcp/protocol.d.ts +6 -0
  186. package/dist/mcp/protocol.js +4 -2
  187. package/dist/mcp/recall-tools.js +19 -6
  188. package/dist/mcp/request.js +4 -0
  189. package/dist/mcp/session-state.d.ts +4 -0
  190. package/dist/mcp/session-state.js +15 -6
  191. package/dist/mcp/shared-gate.d.ts +4 -0
  192. package/dist/mcp/shared-gate.js +22 -0
  193. package/dist/mcp/tools.js +1 -0
  194. package/dist/memory-quality.d.ts +35 -0
  195. package/dist/memory-quality.js +191 -0
  196. package/dist/memory.d.ts +4 -4
  197. package/dist/physics-state.js +1 -1
  198. package/dist/pilot-arm.d.ts +14 -3
  199. package/dist/pilot-arm.js +32 -10
  200. package/dist/postinstall.js +2 -2
  201. package/dist/project-briefs.js +4 -4
  202. package/dist/project-identity.d.ts +14 -10
  203. package/dist/project-identity.js +29 -11
  204. package/dist/project-merge.d.ts +0 -2
  205. package/dist/project-merge.js +9 -13
  206. package/dist/prompt-hook.d.ts +65 -0
  207. package/dist/prompt-hook.js +213 -0
  208. package/dist/prompt-recall.js +1 -1
  209. package/dist/quality-repair.d.ts +27 -0
  210. package/dist/quality-repair.js +220 -0
  211. package/dist/rate-limit.d.ts +1 -0
  212. package/dist/rate-limit.js +5 -3
  213. package/dist/raw-archive.d.ts +1 -1
  214. package/dist/raw-archive.js +6 -16
  215. package/dist/recall-scope.d.ts +28 -5
  216. package/dist/recall-scope.js +52 -7
  217. package/dist/recall-trace.js +1 -1
  218. package/dist/reject-flow.d.ts +2 -0
  219. package/dist/reject-flow.js +20 -6
  220. package/dist/secret-detect.js +7 -8
  221. package/dist/server/auth.d.ts +8 -6
  222. package/dist/server/auth.js +85 -38
  223. package/dist/server/client-ip.d.ts +2 -0
  224. package/dist/server/client-ip.js +16 -1
  225. package/dist/server/mcp-http.d.ts +9 -3
  226. package/dist/server/mcp-http.js +56 -13
  227. package/dist/server/request.d.ts +1 -1
  228. package/dist/server/request.js +5 -5
  229. package/dist/server/routes/admin.js +12 -6
  230. package/dist/server/routes/memories.js +9 -1
  231. package/dist/server/routes/recall.js +14 -2
  232. package/dist/server/types.d.ts +42 -5
  233. package/dist/server/validation.d.ts +5 -2
  234. package/dist/server/validation.js +19 -2
  235. package/dist/server.d.ts +21 -4
  236. package/dist/server.js +207 -49
  237. package/dist/session-digest.d.ts +0 -1
  238. package/dist/session-digest.js +3 -12
  239. package/dist/session-owners.d.ts +7 -0
  240. package/dist/session-owners.js +54 -0
  241. package/dist/share-scrub.d.ts +3 -0
  242. package/dist/share-scrub.js +8 -0
  243. package/dist/shared.d.ts +1 -0
  244. package/dist/shared.js +26 -18
  245. package/dist/store/candidates.d.ts +14 -3
  246. package/dist/store/candidates.js +48 -26
  247. package/dist/store/conflicts.d.ts +15 -0
  248. package/dist/store/conflicts.js +68 -44
  249. package/dist/store/delete-and-batch.d.ts +25 -1
  250. package/dist/store/delete-and-batch.js +121 -69
  251. package/dist/store/entry-reads.d.ts +5 -6
  252. package/dist/store/entry-reads.js +24 -12
  253. package/dist/store/entry-row.d.ts +7 -10
  254. package/dist/store/entry-row.js +40 -18
  255. package/dist/store/entry-writes.d.ts +3 -3
  256. package/dist/store/entry-writes.js +9 -23
  257. package/dist/store/handoffs.d.ts +9 -6
  258. package/dist/store/handoffs.js +72 -59
  259. package/dist/store/index-and-stats.js +2 -2
  260. package/dist/store/open.d.ts +3 -1
  261. package/dist/store/open.js +3 -3
  262. package/dist/store/search-rows.d.ts +8 -5
  263. package/dist/store/search-rows.js +15 -18
  264. package/dist/store/sessions.d.ts +25 -6
  265. package/dist/store/sessions.js +61 -19
  266. package/dist/store/summaries.d.ts +1 -1
  267. package/dist/store/summaries.js +3 -18
  268. package/dist/store/tenant-lookup.d.ts +7 -3
  269. package/dist/store/tenant-lookup.js +9 -9
  270. package/dist/store-port.d.ts +13 -0
  271. package/dist/store-port.js +20 -0
  272. package/dist/version.d.ts +5 -1
  273. package/dist/version.js +5 -1
  274. package/dist/working-memory.js +1 -1
  275. package/dist/write-budget.d.ts +15 -0
  276. package/dist/write-budget.js +15 -0
  277. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  278. package/extensions/openclaw-plugin/package.json +1 -1
  279. package/openclaw.plugin.json +1 -1
  280. package/package.json +8 -4
@@ -18,12 +18,13 @@ import { physicsSearch } from '../search/physics-search.js';
18
18
  import { churnStaleFactor } from '../search/boosts.js';
19
19
  import { compareEntryIdentity } from '../compare.js';
20
20
  import { dropHeldCopies, duplicateKey, storedTextKeys } from '../same-text.js';
21
- import { loadConfig } from '../config.js';
21
+ import { isSharedStore, loadConfig } from '../config.js';
22
+ import { classifyOriginProject, projectNames } from '../project-identity.js';
22
23
  import { computePlanningFallacyOutput } from '../predictions/planning-fallacy.js';
23
24
  import { detectAnchoring, hashQueryText, biasHintEnabled } from '../recall-history.js';
24
25
  import { detectAvailabilityBias } from '../availability.js';
25
- import { passesScopeFilterForRecall, assertScopeRequestAllowed, isRestrictedScope } from '../recall-scope.js';
26
- import { RecallContractError } from './types.js';
26
+ import { passesScopeFilterForRecall, assertScopeRequestAllowed, personalScopeOf } from '../recall-scope.js';
27
+ import { ownerOrSubject, RecallContractError } from './types.js';
27
28
  /**
28
29
  * Shared construction helper for `RecallSuppressionSummary`. Used by
29
30
  * `api.recall`, `cmdRecall`, and the MCP `hippo_recall` handler so all three
@@ -60,44 +61,54 @@ export function recall(ctx, opts) {
60
61
  // A member key may not unlock a private or quarantined scope by naming it.
61
62
  assertScopeRequestAllowed(ctx.actor, opts.scope);
62
63
  const windowSize = recallWindowSize(opts);
63
- return recallFrom(ctx, opts, windowSize, loadRecallSearchEntries(ctx.hippoRoot, opts.query, windowSize, ctx.tenantId, opts.scope, 'exact', false));
64
+ const own = personalScopeOf(ctx.actor) ?? undefined;
65
+ return recallFrom(ctx, opts, windowSize, loadRecallSearchEntries(ctx.hippoRoot, opts.query, windowSize, ctx.tenantId, opts.scope, 'exact', false, recallOrigin(opts), own), own);
66
+ }
67
+ function recallOrigin(opts) {
68
+ return opts.project ? projectNames(opts.project) : undefined;
69
+ }
70
+ function inCallerProject(entry, opts) {
71
+ return !opts.project || classifyOriginProject(entry.origin_project, opts.project) !== 'cross-project';
64
72
  }
65
73
  /** Mode-aware recall that strengthens each returned row; never writes last_retrieval_ids (contract lock). */
66
74
  export async function retrieve(ctx, opts) {
67
75
  assertScopeRequestAllowed(ctx.actor, opts.scope);
68
76
  const windowSize = recallWindowSize(opts);
77
+ // From the authenticated actor, never from RecallOpts, which a caller fills in.
78
+ const own = personalScopeOf(ctx.actor) ?? undefined;
69
79
  if (opts.showRanked)
70
- return retrieveFromStore(ctx, opts, windowSize, opts.showRanked);
71
- let candidates = loadRecallSearchEntries(ctx.hippoRoot, opts.query, windowSize, ctx.tenantId, opts.scope, 'exact', false);
80
+ return retrieveFromStore(ctx, opts, windowSize, opts.showRanked, own);
81
+ let candidates = loadRecallSearchEntries(ctx.hippoRoot, opts.query, windowSize, ctx.tenantId, opts.scope, 'exact', false, recallOrigin(opts), own);
72
82
  if (opts.mode === 'hybrid' || opts.mode === 'physics') {
73
- const searchOpts = { budget: Infinity, hippoRoot: ctx.hippoRoot, scope: opts.scope ?? null, vectorCandidates: recallVectorSpec(ctx, opts) };
83
+ const searchOpts = { budget: Infinity, hippoRoot: ctx.hippoRoot, scope: opts.scope ?? null, vectorCandidates: recallVectorSpec(ctx, opts, own) };
74
84
  const ranked = opts.mode === 'physics'
75
85
  ? await physicsSearch(opts.query, candidates, { ...searchOpts, physicsConfig: loadConfig(ctx.hippoRoot).physics })
76
86
  : await hybridSearch(opts.query, candidates, searchOpts);
77
87
  const rankedIds = new Set(ranked.map((r) => r.entry.id));
78
88
  candidates = [...ranked.map((r) => r.entry), ...candidates.filter((e) => !rankedIds.has(e.id))];
79
89
  }
80
- const result = recallFrom(ctx, opts, windowSize, candidates);
90
+ const result = recallFrom(ctx, opts, windowSize, candidates, own);
81
91
  strengthenRetrieved(ctx.hippoRoot, result.results.map((r) => r.id), { tenantId: ctx.tenantId, recallBoostAblated: isRecallBoostAblated() });
82
92
  return result;
83
93
  }
84
94
  /** api.retrieve's vector arm: the recall load's exact-scope rule, current rows only. */
85
- function recallVectorSpec(ctx, opts) {
95
+ function recallVectorSpec(ctx, opts, own) {
86
96
  return {
87
97
  tenantId: ctx.tenantId,
88
- scope: recallScopeFilter(opts.scope, 'exact'),
98
+ scope: recallScopeFilter(opts.scope, 'exact', own),
89
99
  includeSuperseded: false,
90
- admit: (e) => passesScopeFilterForRecall(e.scope ?? null, opts.scope),
100
+ origin: recallOrigin(opts),
101
+ admit: (e) => passesScopeFilterForRecall(e.scope ?? null, opts.scope, own),
91
102
  };
92
103
  }
93
104
  // Tag, pin and recency boosts can lift a row from deep in the BM25 order, so MCP ranks a wide lexical window.
94
105
  const SHOW_RANKED_LEXICAL_WINDOW = 1000;
95
106
  /** `retrieve` under `showRanked`: physics when `mode` says so, hybrid otherwise, over a wide lexical window plus the nearest vectors. */
96
- async function retrieveFromStore(ctx, opts, windowSize, show) {
97
- const loaded = loadRecallSearchEntries(ctx.hippoRoot, opts.query, Math.max(windowSize, SHOW_RANKED_LEXICAL_WINDOW), ctx.tenantId, opts.scope, 'exact', false);
98
- const pool = loaded.filter((e) => passesScopeFilterForRecall(e.scope ?? null, opts.scope));
107
+ async function retrieveFromStore(ctx, opts, windowSize, show, own) {
108
+ const loaded = loadRecallSearchEntries(ctx.hippoRoot, opts.query, Math.max(windowSize, SHOW_RANKED_LEXICAL_WINDOW), ctx.tenantId, opts.scope, 'exact', false, recallOrigin(opts), own);
109
+ const pool = loaded.filter((e) => passesScopeFilterForRecall(e.scope ?? null, opts.scope, own));
99
110
  // No scope option: the scope boost follows HIPPO_SCOPE and the skill env, as MCP recall always ranked.
100
- const searchOpts = { budget: Infinity, hippoRoot: ctx.hippoRoot, vectorCandidates: recallVectorSpec(ctx, opts) };
111
+ const searchOpts = { budget: Infinity, hippoRoot: ctx.hippoRoot, vectorCandidates: recallVectorSpec(ctx, opts, own) };
101
112
  let ranked = opts.mode === 'physics'
102
113
  ? await physicsSearch(opts.query, pool, { ...searchOpts, physicsConfig: loadConfig(ctx.hippoRoot).physics })
103
114
  : await hybridSearch(opts.query, pool, searchOpts);
@@ -111,7 +122,7 @@ async function retrieveFromStore(ctx, opts, windowSize, show) {
111
122
  }
112
123
  }
113
124
  const window = ranked.slice(0, windowSize).map((r) => r.entry);
114
- const result = recallFrom(ctx, { ...opts, suppressRecallTrace: true }, windowSize, window);
125
+ const result = recallFrom(ctx, { ...opts, suppressRecallTrace: true }, windowSize, window, own);
115
126
  // Rows the vector arm added count as candidates too.
116
127
  const inPool = new Set(pool.map((e) => e.id));
117
128
  const candidates = [...pool, ...ranked.map((r) => r.entry).filter((e) => !inPool.has(e.id))];
@@ -151,14 +162,14 @@ function recallWindowSize(opts) {
151
162
  }
152
163
  return opts.scorerWindow ?? DEFAULT_SEARCH_CANDIDATE_LIMIT;
153
164
  }
154
- function recallFrom(ctx, opts, windowSize, all) {
165
+ function recallFrom(ctx, opts, windowSize, all, own) {
155
166
  const limit = opts.limit ?? 10;
156
- const window = admitCandidates(opts, all, limit);
167
+ const window = admitCandidates(opts, all, limit, own);
157
168
  // One db handle spans the goal-stack boost and the audit and trace rows; it closes before the continuity block.
158
169
  const db = openHippoDb(ctx.hippoRoot);
159
170
  let bands;
160
171
  try {
161
- bands = rankBands(db, ctx, opts, window, limit);
172
+ bands = rankBands(db, ctx, opts, window, limit, own);
162
173
  auditAndTraceRecall(db, ctx, opts, bands.rankedOut);
163
174
  }
164
175
  finally {
@@ -166,7 +177,7 @@ function recallFrom(ctx, opts, windowSize, all) {
166
177
  }
167
178
  const rankedOut = bands.rankedOut;
168
179
  const { continuity, continuityTokens } = opts.includeContinuity
169
- ? loadContinuity(ctx, opts)
180
+ ? loadContinuity(ctx, opts, own)
170
181
  : { continuity: undefined, continuityTokens: undefined };
171
182
  // Query-derived, so MCP and CLI read this one hint instead of recomputing; HIPPO_AUTODEBIAS=off disables it.
172
183
  // The hint and the no-class-match / tiebreak watching variant are mutually exclusive; both go out as optional fields.
@@ -202,7 +213,7 @@ function recallFrom(ctx, opts, windowSize, all) {
202
213
  return result;
203
214
  }
204
215
  // The SQL load already applied tenant and scope; any recall-mode loader must go through loadRecallSearchEntries.
205
- function admitCandidates(opts, all, limit) {
216
+ function admitCandidates(opts, all, limit, own) {
206
217
  const current = all.filter((e) => !e.superseded_by);
207
218
  let entries;
208
219
  if (opts.scope !== undefined && opts.scope !== '') {
@@ -211,7 +222,7 @@ function admitCandidates(opts, all, limit) {
211
222
  }
212
223
  else {
213
224
  // SQL pre-filtered ':private:' loosely before the window; this is the exact anchored `<source>:private:*` rule.
214
- entries = current.filter((e) => !isRestrictedScope(e.scope ?? null));
225
+ entries = current.filter((e) => passesScopeFilterForRecall(e.scope ?? null, undefined, own));
215
226
  }
216
227
  const droppedPreRank = all.length - entries.length;
217
228
  entries = entries
@@ -223,7 +234,7 @@ function admitCandidates(opts, all, limit) {
223
234
  return { entries, baseSlice, droppedPreRank, droppedByBudget: entries.length - baseSlice.length };
224
235
  }
225
236
  // The goal-stack boost touches the primary band only; the fresh-tail and summary bands keep their fixed placement.
226
- function rankBands(db, ctx, opts, window, limit) {
237
+ function rankBands(db, ctx, opts, window, limit, own) {
227
238
  let baseScored = window.baseSlice.map((entry, idx) => ({
228
239
  entry,
229
240
  score: Math.max(0, 1 - idx / Math.max(1, limit)),
@@ -239,7 +250,7 @@ function rankBands(db, ctx, opts, window, limit) {
239
250
  });
240
251
  }
241
252
  let substituted = (opts.summarizeOverflow ?? true) && window.entries.length > limit
242
- ? substituteOverflow(ctx, opts, window.entries, baseScored.map((r) => r.entry), limit)
253
+ ? substituteOverflow(ctx, opts, window.entries, baseScored.map((r) => r.entry), limit, own)
243
254
  : [];
244
255
  let heldDropped = 0;
245
256
  if (!opts.keepHeldCopies) {
@@ -252,7 +263,7 @@ function rankBands(db, ctx, opts, window, limit) {
252
263
  const baseRanked = baseScored.map((r) => baseItem(r, opts, explainTrace));
253
264
  const summaryRanked = substituted.map((s) => summaryItem(s, opts));
254
265
  const freshRanked = (opts.freshTailCount ?? 0) > 0
255
- ? freshTailBand(ctx, opts, baseRanked, summaryRanked, opts.keepHeldCopies ? [] : [...baseSlice, ...substituted.map((s) => s.entry)])
266
+ ? freshTailBand(ctx, opts, baseRanked, summaryRanked, opts.keepHeldCopies ? [] : [...baseSlice, ...substituted.map((s) => s.entry)], own)
256
267
  : [];
257
268
  return {
258
269
  rankedOut: [...freshRanked, ...baseRanked, ...summaryRanked],
@@ -264,7 +275,7 @@ function rankBands(db, ctx, opts, window, limit) {
264
275
  }
265
276
  // When the overflow holds 2+ children of one level-2 summary, that summary stands in for them, capped at 30% of
266
277
  // `limit`. Each one is tenant-scoped and re-checked against the scope filter; drillDown recovers the children.
267
- function substituteOverflow(ctx, opts, entries, baseSlice, limit) {
278
+ function substituteOverflow(ctx, opts, entries, baseSlice, limit, own) {
268
279
  const overflow = entries.slice(limit);
269
280
  const baseIds = new Set(baseSlice.map((e) => e.id));
270
281
  const overflowByParent = new Map();
@@ -282,7 +293,7 @@ function substituteOverflow(ctx, opts, entries, baseSlice, limit) {
282
293
  if (eligibleParentIds.length === 0)
283
294
  return [];
284
295
  const parents = loadEntriesByIds(ctx.hippoRoot, eligibleParentIds, ctx.tenantId);
285
- const eligibleParents = parents.filter((p) => (p.dag_level ?? 0) === 2 && !p.superseded_by && passesScopeFilterForRecall(p.scope ?? null, opts.scope));
296
+ const eligibleParents = parents.filter((p) => (p.dag_level ?? 0) === 2 && !p.superseded_by && passesScopeFilterForRecall(p.scope ?? null, opts.scope, own) && inCallerProject(p, opts));
286
297
  const maxSub = Math.max(1, Math.ceil(limit * 0.3));
287
298
  // Most overflowed children first; compareEntryIdentity only breaks a tie, which used to fall to scan order.
288
299
  eligibleParents.sort((a, b) => {
@@ -330,10 +341,10 @@ function summaryItem(s, opts) {
330
341
  }
331
342
  // The last N raw rows, so "what did I just see" always covers the recent window. A recent row already in the BM25
332
343
  // band is only tagged isFreshTail; new ones are prepended, so every recent row appears exactly once.
333
- function freshTailBand(ctx, opts, baseRanked, summaryRanked, shownEntries) {
344
+ function freshTailBand(ctx, opts, baseRanked, summaryRanked, shownEntries, own) {
334
345
  // The session-id contract was already checked by recallWindowSize's preflight.
335
- const recent = loadFreshRawMemories(ctx.hippoRoot, opts.freshTailCount ?? 0, ctx.tenantId, opts.freshTailSessionId);
336
- const recentScoped = recent.filter((m) => passesScopeFilterForRecall(m.scope ?? null, opts.scope));
346
+ const recent = loadFreshRawMemories(ctx.hippoRoot, opts.freshTailCount ?? 0, ctx.tenantId, opts.freshTailSessionId, recallOrigin(opts));
347
+ const recentScoped = recent.filter((m) => passesScopeFilterForRecall(m.scope ?? null, opts.scope, own));
337
348
  const recentIdSet = new Set(recentScoped.map((m) => m.id));
338
349
  for (const r of baseRanked) {
339
350
  if (recentIdSet.has(r.id))
@@ -389,20 +400,24 @@ function auditAndTraceRecall(db, ctx, opts, rankedOut) {
389
400
  }
390
401
  }
391
402
  // No active snapshot means no anchor, so no handoff or events: a stale handoff from a closed session never resurfaces.
392
- function loadContinuity(ctx, opts) {
393
- const snapshot = loadActiveTaskSnapshot(ctx.hippoRoot, ctx.tenantId);
403
+ function loadContinuity(ctx, opts, own) {
404
+ // On a shared store the tenant's newest row is another developer's; no project keys to nothing, so the block is empty.
405
+ const key = isSharedStore(ctx.hippoRoot)
406
+ ? { owner: ownerOrSubject(ctx.actor), project: opts.project ? projectNames(opts.project) : [] }
407
+ : undefined;
408
+ const snapshot = loadActiveTaskSnapshot(ctx.hippoRoot, ctx.tenantId, key);
394
409
  const sessionId = snapshot?.session_id ?? undefined;
395
410
  const sessionHandoff = sessionId
396
- ? loadLatestHandoff(ctx.hippoRoot, ctx.tenantId, sessionId)
411
+ ? loadLatestHandoff(ctx.hippoRoot, ctx.tenantId, sessionId, {}, key)
397
412
  : null;
398
413
  const recentSessionEvents = sessionId
399
414
  ? listSessionEvents(ctx.hippoRoot, ctx.tenantId, { session_id: sessionId, limit: 5 })
400
415
  : [];
401
416
  // The memory-recall scope rule: an explicit scope must match exactly; without one, private and legacy rows are denied.
402
417
  const rowScope = (r) => r?.scope ?? null;
403
- const filteredSnapshot = snapshot && passesScopeFilterForRecall(rowScope(snapshot), opts.scope) ? snapshot : null;
404
- const filteredHandoff = sessionHandoff && passesScopeFilterForRecall(rowScope(sessionHandoff), opts.scope) ? sessionHandoff : null;
405
- const filteredEvents = recentSessionEvents.filter((e) => passesScopeFilterForRecall(rowScope(e), opts.scope));
418
+ const filteredSnapshot = snapshot && passesScopeFilterForRecall(rowScope(snapshot), opts.scope, own) ? snapshot : null;
419
+ const filteredHandoff = sessionHandoff && passesScopeFilterForRecall(rowScope(sessionHandoff), opts.scope, own) ? sessionHandoff : null;
420
+ const filteredEvents = recentSessionEvents.filter((e) => passesScopeFilterForRecall(rowScope(e), opts.scope, own));
406
421
  const continuity = {
407
422
  activeSnapshot: filteredSnapshot,
408
423
  sessionHandoff: filteredHandoff,
@@ -19,6 +19,13 @@ export interface RememberOpts {
19
19
  afterWrite?: (db: DatabaseSyncLike, memoryId: string) => void;
20
20
  /** Connector-ingested content an agent doesn't control; gates detectInstruction. CLI/HTTP/MCP never set this. */
21
21
  untrusted?: boolean;
22
+ /** The caller's project: its name becomes the row's origin. Without it the store's fallback applies (NULL on a shared store). */
23
+ project?: {
24
+ readonly name: string;
25
+ readonly aliases?: readonly string[];
26
+ };
27
+ /** Store it in the caller's own personal scope, readable by its owner alone in every project; needs an owner on the actor and no `scope`. */
28
+ personal?: boolean;
22
29
  }
23
30
  export interface RememberResult {
24
31
  id: string;
@@ -5,10 +5,31 @@ import { quarantineScopeFor, recordQuarantine } from '../quarantine.js';
5
5
  import { createMemory } from '../memory.js';
6
6
  import { loadConfig } from '../config.js';
7
7
  import { vetSecrets } from '../secret-detect.js';
8
+ import { assertCallerProject } from '../project-identity.js';
9
+ import { BadRequestError } from '../api-errors.js';
10
+ import { assertClientScope, personalScopeOf } from '../recall-scope.js';
11
+ /** The scope a write lands in: the caller's own personal scope when asked for, else the client's, which may not be a personal one. */
12
+ function rememberScope(ctx, opts) {
13
+ if (!opts.personal) {
14
+ assertClientScope(opts.scope);
15
+ return opts.scope ?? null;
16
+ }
17
+ if (opts.untrusted)
18
+ throw new Error('personal memories come from their owner, never from a connector');
19
+ if (opts.scope !== undefined)
20
+ throw new BadRequestError('send personal or scope, not both');
21
+ const own = personalScopeOf(ctx.actor);
22
+ if (own === null) {
23
+ throw new BadRequestError('personal memories need a key its owner minted, or a sign-in; owner ids over 239 characters or with control characters cannot hold them');
24
+ }
25
+ return own;
26
+ }
8
27
  export function remember(ctx, opts) {
28
+ if (opts.project)
29
+ assertCallerProject(opts.project);
30
+ const requestedScope = rememberScope(ctx, opts);
9
31
  const vetted = vetSecrets(opts.content, opts.tags ?? [], opts.untrusted === true);
10
32
  const detection = opts.untrusted ? detectInstruction(vetted.content) : { flagged: false, reason: null };
11
- const requestedScope = opts.scope ?? null;
12
33
  const entry = createMemory(vetted.content, {
13
34
  kind: opts.kind ?? 'distilled',
14
35
  scope: detection.flagged ? quarantineScopeFor(requestedScope) : requestedScope,
@@ -32,7 +53,10 @@ export function remember(ctx, opts) {
32
53
  opts.afterWrite?.(db, memoryId);
33
54
  }
34
55
  : opts.afterWrite;
35
- writeEntry(ctx.hippoRoot, entry, { actor: ctx.actor.subject, afterWrite });
56
+ // Aliases only widen what a reader matches; a row has one origin. A personal row's '' follows its owner into every project.
57
+ const origin = opts.personal ? '' : opts.project?.name;
58
+ const stamped = origin !== undefined ? { ...entry, origin_project: origin } : entry;
59
+ writeEntry(ctx.hippoRoot, stamped, { actor: ctx.actor.subject, afterWrite });
36
60
  const result = { id: entry.id, kind: entry.kind, tenantId: ctx.tenantId };
37
61
  if (detection.flagged)
38
62
  result.quarantined = { reason: detection.reason ?? 'unknown' };
@@ -0,0 +1,28 @@
1
+ import { loadAllEntries } from '../store/entry-reads.js';
2
+ import { deleteEntry } from '../store/delete-and-batch.js';
3
+ import { auditMemories } from '../audit.js';
4
+ import { autoShare } from '../shared.js';
5
+ import { consolidate } from '../consolidate/sleep.js';
6
+ import { loadConfig } from '../config.js';
7
+ import { deduplicateStore } from '../dedupe.js';
8
+ import { computeAmbientState } from '../ambient.js';
9
+ import { loadPendingExtractionTenants } from '../graph/read.js';
10
+ import { type ExtractResult } from '../graph-extract.js';
11
+ import type { Context } from './types.js';
12
+ import type { SleepOpts, SleepResult } from './sleep.js';
13
+ /** Test-only seam: `runSleep` overrides force a phase to throw into emitSleepAudit's `partial: true` row; production never sets them. */
14
+ export interface SleepPhases {
15
+ consolidate: typeof consolidate;
16
+ deduplicateStore: typeof deduplicateStore;
17
+ auditMemories: typeof auditMemories;
18
+ autoShare: typeof autoShare;
19
+ loadAllEntries: typeof loadAllEntries;
20
+ deleteEntry: typeof deleteEntry;
21
+ computeAmbientState: typeof computeAmbientState;
22
+ loadConfig: typeof loadConfig;
23
+ loadPendingExtractionTenants: typeof loadPendingExtractionTenants;
24
+ extractGraph: (hippoRoot: string, tenantId: string) => ExtractResult | Promise<ExtractResult>;
25
+ }
26
+ /** `sleep` with its phase dependencies overridable; tests pass stubs here, so the seam never touches the public `SleepOpts`. */
27
+ export declare function runSleep(ctx: Context, opts: SleepOpts, overrides?: Partial<SleepPhases>): Promise<SleepResult>;
28
+ //# sourceMappingURL=sleep-run.d.ts.map
@@ -0,0 +1,267 @@
1
+ // The sleep pipeline behind `sleep`, with its phase dependencies injectable; kept out of the package root.
2
+ import { openHippoDb, closeHippoDb } from '../db.js';
3
+ import { loadAllEntries } from '../store/entry-reads.js';
4
+ import { deleteEntry, memoriesBackingObjects } from '../store/delete-and-batch.js';
5
+ import { appendAuditEvent, reportAuditWriteFailure, auditMemories } from '../audit.js';
6
+ import { autoShare } from '../shared.js';
7
+ import { consolidate } from '../consolidate/sleep.js';
8
+ import { failedUnitOf } from '../store/delete-and-batch.js';
9
+ import { loadConfig } from '../config.js';
10
+ import { deduplicateStore } from '../dedupe.js';
11
+ import { computeAmbientState } from '../ambient.js';
12
+ import { loadPendingExtractionTenants } from '../graph/read.js';
13
+ import { markPendingProcessedUpTo } from '../graph/write.js';
14
+ import { extractGraphChunked } from '../graph-extract.js';
15
+ const DEFAULT_SLEEP_PHASES = {
16
+ consolidate,
17
+ deduplicateStore,
18
+ auditMemories,
19
+ autoShare,
20
+ loadAllEntries,
21
+ deleteEntry,
22
+ computeAmbientState,
23
+ loadConfig,
24
+ loadPendingExtractionTenants,
25
+ extractGraph: extractGraphChunked,
26
+ };
27
+ /** `sleep` with its phase dependencies overridable; tests pass stubs here, so the seam never touches the public `SleepOpts`. */
28
+ export async function runSleep(ctx, opts, overrides = {}) {
29
+ const dryRun = Boolean(opts.dryRun);
30
+ const phases = { ...DEFAULT_SLEEP_PHASES, ...overrides };
31
+ // Phase counters for the consolidate audit emit (in finally).
32
+ // Accumulated as each phase completes so partial-failure paths still report
33
+ // accurate "what got done before the failure" data.
34
+ const counts = { consolidation: 0, dedup: 0, auditDeleted: 0, ambient: 0 };
35
+ let phaseError = null;
36
+ try {
37
+ return await runSleepPhases(ctx, opts, phases, counts);
38
+ }
39
+ catch (err) {
40
+ // SAFETY: phaseError is read via phaseError.message / (phaseError !==
41
+ // null) below, both safe even if a non-Error was thrown; this mirrors
42
+ // the existing lenient (err as Error) pattern used throughout this catch chain.
43
+ phaseError = err;
44
+ throw err;
45
+ }
46
+ finally {
47
+ emitSleepAudit(ctx, opts, dryRun, counts, phaseError);
48
+ }
49
+ }
50
+ async function runSleepPhases(ctx, opts, phases, counts) {
51
+ const dryRun = Boolean(opts.dryRun);
52
+ const snapshot = snapshotDirtyTenants(ctx, phases, dryRun);
53
+ // Phase 1: Consolidation.
54
+ const consolidateResult = await phases.consolidate(ctx.hippoRoot, { dryRun });
55
+ counts.consolidation = consolidateResult.semanticCreated + consolidateResult.merged;
56
+ const result = sleepResultFrom(consolidateResult, dryRun);
57
+ // Phase 2: Dedup (post-consolidate near-duplicate cleanup).
58
+ const dedupResult = phases.deduplicateStore(ctx.hippoRoot, { dryRun, actor: ctx.actor.subject });
59
+ counts.dedup = dedupResult.removed;
60
+ if (dedupResult.removed > 0)
61
+ result.deduped = dedupSummary(dedupResult);
62
+ // Phase 3: Quality audit (remove junk, report warnings; a dry run skips rows earlier phases would remove).
63
+ counts.auditDeleted = runQualityAudit(ctx, phases, dryRun, consolidateResult, dedupResult, result);
64
+ if (dryRun)
65
+ return result;
66
+ // Phase 4: Auto-share high-transfer-score memories to global.
67
+ if (!opts.noShare)
68
+ shareOnSleep(ctx, phases, result);
69
+ // Phase 5: Post-sleep ambient state summary.
70
+ counts.ambient = summarizeAmbient(ctx, phases, result);
71
+ await drainGraphQueue(ctx, phases, snapshot, result);
72
+ return result;
73
+ }
74
+ // Taken before any memory-deleting phase: queue rows cascade-delete with their mirror, so a later read would miss
75
+ // tenants dedup touched. Fail-soft: a failed read skips the graph refresh this sleep and never aborts core sleep.
76
+ function snapshotDirtyTenants(ctx, phases, dryRun) {
77
+ const snapshot = { dirtyTenants: [], error: null };
78
+ if (!dryRun) {
79
+ try {
80
+ snapshot.dirtyTenants = phases.loadPendingExtractionTenants(ctx.hippoRoot);
81
+ }
82
+ catch (snapErr) {
83
+ // SAFETY: this is a best-effort log message only; property access on
84
+ // any JS value is safe (undefined if absent), preserving the existing
85
+ // lenient formatting even when something non-Error was thrown.
86
+ snapshot.error = snapErr.message;
87
+ }
88
+ }
89
+ return snapshot;
90
+ }
91
+ function sleepResultFrom(consolidateResult, dryRun) {
92
+ const result = {
93
+ active: consolidateResult.decayed,
94
+ removed: consolidateResult.removed,
95
+ mergedEpisodic: consolidateResult.merged,
96
+ newSemantic: consolidateResult.semanticCreated,
97
+ dryRun,
98
+ details: consolidateResult.details,
99
+ };
100
+ // Set only when non-zero, so a store without dormant memories gets a
101
+ // byte-identical result (HTTP /v1/sleep, the CLI render snapshot).
102
+ if (consolidateResult.dormant > 0) {
103
+ result.dormant = consolidateResult.dormant;
104
+ }
105
+ if (consolidateResult.dormantExpired > 0) {
106
+ result.dormantExpired = consolidateResult.dormantExpired;
107
+ }
108
+ return result;
109
+ }
110
+ function dedupSummary(dedupResult) {
111
+ const semDups = dedupResult.pairs.filter((p) => p.keptLayer === 'semantic' && p.removedLayer === 'semantic').length;
112
+ const epiDups = dedupResult.pairs.filter((p) => p.keptLayer === 'episodic' && p.removedLayer === 'episodic').length;
113
+ const crossDups = dedupResult.pairs.filter((p) => p.keptLayer !== p.removedLayer).length;
114
+ return {
115
+ removed: dedupResult.removed,
116
+ semDups,
117
+ epiDups,
118
+ crossDups,
119
+ };
120
+ }
121
+ /** Returns how many audit errors were deleted, or would be under dryRun. */
122
+ function runQualityAudit(ctx, phases, dryRun, consolidateResult, dedupResult, result) {
123
+ const planned = new Set(dryRun ? [...(consolidateResult.removedIds ?? []), ...dedupResult.pairs.map((p) => p.removed)] : []);
124
+ const allEntries = phases.loadAllEntries(ctx.hippoRoot).filter((e) => !planned.has(e.id));
125
+ const auditOut = phases.auditMemories(allEntries, memoriesBackingObjects(ctx.hippoRoot));
126
+ if (auditOut.issues.length === 0)
127
+ return 0;
128
+ const errors = auditOut.issues.filter((i) => i.severity === 'error');
129
+ const warnings = auditOut.issues.filter((i) => i.severity === 'warning');
130
+ let removed = 0;
131
+ for (const issue of errors) {
132
+ const reason = `sleep-audit: ${issue.reason}`;
133
+ if (dryRun || phases.deleteEntry(ctx.hippoRoot, issue.memoryId, { actor: ctx.actor.subject, reason, automatic: true }))
134
+ removed++;
135
+ }
136
+ if (removed > 0 || warnings.length > 0) {
137
+ result.audit = {
138
+ errorsRemoved: removed,
139
+ warningCount: warnings.length,
140
+ };
141
+ }
142
+ return removed;
143
+ }
144
+ function shareOnSleep(ctx, phases, result) {
145
+ const sleepConfig = phases.loadConfig(ctx.hippoRoot);
146
+ if (!sleepConfig.autoShareOnSleep)
147
+ return;
148
+ // Both skip counters are surfaced so the secret veto and the rejection tombstone are observable, not silent.
149
+ const autoShareStats = { secretSkipped: 0, rejectedSkipped: 0 };
150
+ const shared = phases.autoShare(ctx.hippoRoot, { minScore: 0.6, stats: autoShareStats });
151
+ if (shared.length > 0) {
152
+ result.shared = shared.length;
153
+ }
154
+ if (autoShareStats.secretSkipped > 0) {
155
+ result.secretSkipped = autoShareStats.secretSkipped;
156
+ }
157
+ if (autoShareStats.rejectedSkipped > 0) {
158
+ result.rejectedSkipped = autoShareStats.rejectedSkipped;
159
+ }
160
+ }
161
+ /** Returns the ambient total the audit row reports: 0 when ambient is off or no current row is left. */
162
+ function summarizeAmbient(ctx, phases, result) {
163
+ const postSleepConfig = phases.loadConfig(ctx.hippoRoot);
164
+ if (!postSleepConfig.ambient.enabled)
165
+ return 0;
166
+ const postSleepEntries = phases.loadAllEntries(ctx.hippoRoot).filter((e) => !e.superseded_by);
167
+ if (postSleepEntries.length === 0)
168
+ return 0;
169
+ result.ambient = phases.computeAmbientState(postSleepEntries);
170
+ return result.ambient.totalMemories;
171
+ }
172
+ // Phase 6: rebuild the graph of every tenant marked dirty since the last sleep, so graph recall runs on fresh data.
173
+ // Fault-isolated: consolidation has already committed, so no failure here aborts sleep; a failed tenant stays pending.
174
+ async function drainGraphQueue(ctx, phases, snapshot, result) {
175
+ try {
176
+ if (snapshot.error) {
177
+ // Core sleep already succeeded; surface the skipped graph refresh as a detail.
178
+ result.details = [
179
+ ...(result.details ?? []),
180
+ `graph: dirty-tenant snapshot failed (skipped graph refresh): ${snapshot.error}`,
181
+ ];
182
+ }
183
+ let gTenants = 0;
184
+ let gEntities = 0;
185
+ let gRelations = 0;
186
+ // dirtyTenants was snapshotted before the memory-deleting phases above.
187
+ for (const { tenantId, maxPendingId } of snapshot.dirtyTenants) {
188
+ try {
189
+ const ext = await phases.extractGraph(ctx.hippoRoot, tenantId);
190
+ // Count the rebuild as soon as it succeeds — it happened regardless of
191
+ // the drain-mark below.
192
+ gTenants += 1;
193
+ gEntities += ext.entities;
194
+ gRelations += ext.relations;
195
+ if (ext.skipped) {
196
+ result.details = [
197
+ ...(result.details ?? []),
198
+ `graph: ${ext.skipped} stale op(s) skipped for a tenant; the next sleep redoes them`,
199
+ ];
200
+ }
201
+ // Watermark drain: only items enqueued before this rebuild started are marked; later arrivals stay pending.
202
+ // Marked only after the last chunk, so a run stopped between chunks leaves the tenant for the next one.
203
+ markPendingProcessedUpTo(ctx.hippoRoot, tenantId, maxPendingId);
204
+ }
205
+ catch (tenantErr) {
206
+ // SAFETY: this is a best-effort log message only; property access
207
+ // on any JS value is safe (undefined if absent), preserving the
208
+ // existing lenient formatting even when something non-Error was thrown.
209
+ result.details = [
210
+ ...(result.details ?? []),
211
+ `graph: extract failed for a dirty tenant (left pending): ${tenantErr.message}`,
212
+ ];
213
+ }
214
+ }
215
+ if (gTenants > 0) {
216
+ result.graph = { tenants: gTenants, entities: gEntities, relations: gRelations };
217
+ }
218
+ }
219
+ catch (graphErr) {
220
+ // SAFETY: this is a best-effort log message only; property access on
221
+ // any JS value is safe (undefined if absent), preserving the existing
222
+ // lenient formatting even when something non-Error was thrown.
223
+ result.details = [
224
+ ...(result.details ?? []),
225
+ `graph: drain phase failed (skipped): ${graphErr.message}`,
226
+ ];
227
+ }
228
+ }
229
+ // One 'consolidate' row per sleep, emitted from finally so a partial failure still reports what got done.
230
+ // Its own handle; an audit failure is logged and never replaces the original phase error.
231
+ function emitSleepAudit(ctx, opts, dryRun, counts, phaseError) {
232
+ try {
233
+ const db = openHippoDb(ctx.hippoRoot);
234
+ try {
235
+ const sleepAuditMetadata = {
236
+ consolidationCount: counts.consolidation,
237
+ dedupCount: counts.dedup,
238
+ auditDeletedCount: counts.auditDeleted,
239
+ ambientTotal: counts.ambient,
240
+ dryRun,
241
+ noShare: opts.noShare ?? false,
242
+ partial: phaseError !== null,
243
+ triggeredByTenant: ctx.tenantId, // preserve for audit forensics
244
+ };
245
+ if (phaseError)
246
+ sleepAuditMetadata.errorMessage = phaseError.message;
247
+ // A flush stopped between chunks names the unit it would have committed next, so a unit that fails every night can be found.
248
+ const nextUnitIds = failedUnitOf(phaseError);
249
+ if (nextUnitIds)
250
+ sleepAuditMetadata.nextUnitIds = nextUnitIds;
251
+ appendAuditEvent(db, {
252
+ tenantId: '__host__',
253
+ actor: ctx.actor.subject,
254
+ op: 'consolidate',
255
+ metadata: { ...sleepAuditMetadata },
256
+ });
257
+ }
258
+ finally {
259
+ closeHippoDb(db);
260
+ }
261
+ }
262
+ catch (auditErr) {
263
+ // Logged, never thrown: a second failure must not mask the original phaseError.
264
+ reportAuditWriteFailure('consolidate', String(auditErr));
265
+ }
266
+ }
267
+ //# sourceMappingURL=sleep-run.js.map
@@ -1,38 +1,11 @@
1
- import { loadAllEntries } from '../store/entry-reads.js';
2
- import { deleteEntry } from '../store/delete-and-batch.js';
3
- import { auditMemories } from '../audit.js';
4
- import { autoShare } from '../shared.js';
5
- import { consolidate } from '../consolidate/sleep.js';
6
- import { loadConfig } from '../config.js';
7
- import { deduplicateStore } from '../dedupe.js';
8
- import { computeAmbientState, type AmbientState } from '../ambient.js';
9
- import { loadPendingExtractionTenants } from '../graph/read.js';
10
- import { extractGraph } from '../graph-extract.js';
1
+ import type { AmbientState } from '../ambient.js';
11
2
  import type { Context } from './types.js';
12
- /**
13
- * Options for `sleep` — run the pure-storage consolidation pipeline
14
- * (consolidate + dedup + audit + share + ambient) and return structured counts.
15
- *
16
- * Extracted from `cmdSleepCore` Phase 2-6 in Episode A. NOT covered by api.sleep:
17
- * the cli-only auto-learn phase (Phase 1: learnFromRepo + the agent memory import),
18
- * which is intrinsically host-bound (uses `process.cwd()` / `os.homedir()`).
19
- * Auto-learn stays in cli.ts cmdSleepCore as a pre-api block.
20
- *
21
- * The CLI `cmdSleep` wrapper continues to own the log-file tee + console
22
- * rendering + `process.exit`; `api.sleep` is pure (no console.log, no IO
23
- * beyond the store).
24
- */
3
+ /** Options for `sleep`, one consolidation cycle on a SQLite store that returns counts; unlike `hippo sleep` it skips the auto-learn step. */
25
4
  export interface SleepOpts {
5
+ /** Report what a sleep would change without applying it; sharing, the ambient summary and the graph refresh are skipped. */
26
6
  dryRun?: boolean;
7
+ /** Skip copying high-scoring memories to the global store under `HIPPO_HOME`. Default false, so every tenant's memories are eligible; multi-tenant callers pass `true`. */
27
8
  noShare?: boolean;
28
- /**
29
- * @internal Test-only DI seam — see `tests/api-sleep-phase-faults.test.ts`.
30
- * Override one or more phase dependencies (typically a throwing stub) to
31
- * force mid-phase failure paths deterministically. Production callers
32
- * MUST NOT use this field. The runtime defaults at `DEFAULT_SLEEP_PHASES`
33
- * preserve all current behaviour when `__phases` is undefined.
34
- */
35
- __phases?: Partial<SleepPhases>;
36
9
  }
37
10
  export interface SleepResult {
38
11
  active: number;
@@ -90,19 +63,6 @@ export interface SleepResult {
90
63
  };
91
64
  details?: string[];
92
65
  }
93
- /** Test-only seam: `SleepOpts.__phases` forces a phase to throw into emitSleepAudit's `partial: true` row; production never sets it. */
94
- export interface SleepPhases {
95
- consolidate: typeof consolidate;
96
- deduplicateStore: typeof deduplicateStore;
97
- auditMemories: typeof auditMemories;
98
- autoShare: typeof autoShare;
99
- loadAllEntries: typeof loadAllEntries;
100
- deleteEntry: typeof deleteEntry;
101
- computeAmbientState: typeof computeAmbientState;
102
- loadConfig: typeof loadConfig;
103
- loadPendingExtractionTenants: typeof loadPendingExtractionTenants;
104
- extractGraph: typeof extractGraph;
105
- }
106
- /** Sleeps the WHOLE hippoRoot, every tenant, so /v1/sleep stays loopback-only; never auto-deletes pinned, raw, kept or object-backing rows. */
66
+ /** Sleeps the WHOLE hippoRoot, every tenant, so /v1/sleep stays loopback-only; SQLite only; never auto-deletes pinned, raw, kept or object-backing rows. */
107
67
  export declare function sleep(ctx: Context, opts?: SleepOpts): Promise<SleepResult>;
108
68
  //# sourceMappingURL=sleep.d.ts.map