hippo-memory 1.61.0 → 1.63.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 (377) hide show
  1. package/README.md +37 -53
  2. package/dist/agent-memories/apply.d.ts +3 -1
  3. package/dist/agent-memories/apply.js +23 -1
  4. package/dist/agent-memories/claude-code.d.ts +4 -1
  5. package/dist/agent-memories/claude-code.js +53 -9
  6. package/dist/agent-memories/report.d.ts +1 -0
  7. package/dist/agent-memories/report.js +2 -2
  8. package/dist/agent-memories/sync.d.ts +3 -3
  9. package/dist/agent-memories/sync.js +46 -24
  10. package/dist/agent-memories/types.d.ts +0 -2
  11. package/dist/ambient-store.d.ts +4 -4
  12. package/dist/ambient-store.js +4 -3
  13. package/dist/api/assemble.d.ts +7 -10
  14. package/dist/api/assemble.js +62 -66
  15. package/dist/api/audit.d.ts +2 -2
  16. package/dist/api/audit.js +2 -2
  17. package/dist/api/auth.d.ts +6 -6
  18. package/dist/api/auth.js +5 -7
  19. package/dist/api/context-select.d.ts +50 -0
  20. package/dist/api/context-select.js +342 -0
  21. package/dist/api/context-types.d.ts +12 -17
  22. package/dist/api/context.d.ts +5 -4
  23. package/dist/api/context.js +208 -535
  24. package/dist/api/dormant.js +1 -1
  25. package/dist/api/drill-down.d.ts +4 -4
  26. package/dist/api/drill-down.js +56 -41
  27. package/dist/api/outcome.d.ts +8 -13
  28. package/dist/api/outcome.js +13 -17
  29. package/dist/api/promote.d.ts +5 -8
  30. package/dist/api/promote.js +55 -66
  31. package/dist/api/quarantine.js +3 -2
  32. package/dist/api/recall-types.d.ts +43 -55
  33. package/dist/api/recall.d.ts +3 -3
  34. package/dist/api/recall.js +310 -456
  35. package/dist/api/remember.d.ts +2 -2
  36. package/dist/api/sleep.d.ts +7 -35
  37. package/dist/api/sleep.js +206 -220
  38. package/dist/api/tokens.d.ts +2 -2
  39. package/dist/api/tokens.js +2 -2
  40. package/dist/api/types.d.ts +6 -10
  41. package/dist/api/types.js +2 -4
  42. package/dist/audit-prune.d.ts +4 -6
  43. package/dist/audit-prune.js +3 -5
  44. package/dist/audit.js +11 -33
  45. package/dist/auth.d.ts +8 -9
  46. package/dist/auth.js +5 -7
  47. package/dist/autolearn.d.ts +1 -1
  48. package/dist/autolearn.js +1 -1
  49. package/dist/availability.js +3 -5
  50. package/dist/capture/command.d.ts +5 -4
  51. package/dist/capture/command.js +8 -22
  52. package/dist/capture/compact.d.ts +2 -2
  53. package/dist/capture/compact.js +11 -15
  54. package/dist/capture/extract.js +37 -115
  55. package/dist/capture-error.d.ts +1 -1
  56. package/dist/capture-error.js +1 -1
  57. package/dist/churn-git.d.ts +1 -1
  58. package/dist/churn-git.js +1 -1
  59. package/dist/cli/audit.js +3 -4
  60. package/dist/cli/auth.js +3 -6
  61. package/dist/cli/briefs.js +324 -306
  62. package/dist/cli/context.js +44 -34
  63. package/dist/cli/continuity.js +283 -271
  64. package/dist/cli/curate.d.ts +1 -1
  65. package/dist/cli/curate.js +43 -58
  66. package/dist/cli/dag.js +5 -9
  67. package/dist/cli/decisions.js +334 -345
  68. package/dist/cli/explain.js +68 -61
  69. package/dist/cli/goals.js +1 -1
  70. package/dist/cli/init.js +1 -1
  71. package/dist/cli/maintenance.js +62 -51
  72. package/dist/cli/playbooks.js +391 -379
  73. package/dist/cli/projects.js +11 -6
  74. package/dist/cli/recall.js +30 -44
  75. package/dist/cli/remember.js +118 -87
  76. package/dist/cli/session-hooks.js +106 -115
  77. package/dist/cli/setup.d.ts +1 -1
  78. package/dist/cli/setup.js +267 -250
  79. package/dist/cli/shared.js +3 -3
  80. package/dist/cli/slack.js +1 -1
  81. package/dist/cli/sleep.js +27 -1
  82. package/dist/cli/status.d.ts +4 -4
  83. package/dist/cli/status.js +80 -76
  84. package/dist/cli/transfer.js +88 -107
  85. package/dist/cli/usage.js +9 -6
  86. package/dist/cli.d.ts +1 -1
  87. package/dist/cli.js +4 -9
  88. package/dist/compaction-record.d.ts +2 -2
  89. package/dist/compaction-record.js +89 -68
  90. package/dist/compare.d.ts +11 -16
  91. package/dist/compare.js +11 -16
  92. package/dist/config.d.ts +19 -18
  93. package/dist/config.js +90 -67
  94. package/dist/connectors/github/backfill.d.ts +2 -2
  95. package/dist/connectors/github/backfill.js +8 -15
  96. package/dist/connectors/github/cli-impl.js +3 -8
  97. package/dist/connectors/github/deletion.d.ts +5 -12
  98. package/dist/connectors/github/deletion.js +5 -12
  99. package/dist/connectors/github/dlq.d.ts +6 -9
  100. package/dist/connectors/github/dlq.js +2 -3
  101. package/dist/connectors/github/ingest.d.ts +5 -7
  102. package/dist/connectors/github/ingest.js +8 -12
  103. package/dist/connectors/github/octokit-client.d.ts +3 -5
  104. package/dist/connectors/github/octokit-client.js +5 -6
  105. package/dist/connectors/github/signature.d.ts +9 -39
  106. package/dist/connectors/github/signature.js +9 -39
  107. package/dist/connectors/github/tenant-routing.d.ts +1 -1
  108. package/dist/connectors/github/tenant-routing.js +1 -1
  109. package/dist/connectors/github/transform.js +2 -2
  110. package/dist/connectors/github/types.d.ts +2 -10
  111. package/dist/connectors/github/types.js +1 -3
  112. package/dist/connectors/slack/deletion.d.ts +3 -8
  113. package/dist/connectors/slack/deletion.js +3 -8
  114. package/dist/connectors/slack/dlq.d.ts +1 -1
  115. package/dist/connectors/slack/ingest.d.ts +1 -1
  116. package/dist/connectors/slack/ingest.js +7 -16
  117. package/dist/connectors/slack/signature.d.ts +1 -1
  118. package/dist/connectors/slack/tenant-routing.d.ts +3 -5
  119. package/dist/connectors/slack/tenant-routing.js +3 -5
  120. package/dist/connectors/slack/transform.d.ts +5 -6
  121. package/dist/connectors/slack/transform.js +5 -6
  122. package/dist/connectors/slack/types.d.ts +2 -6
  123. package/dist/connectors/slack/types.js +1 -3
  124. package/dist/connectors/slack/web-client.js +10 -3
  125. package/dist/connectors/slack/workspaces.d.ts +3 -5
  126. package/dist/connectors/slack/workspaces.js +3 -5
  127. package/dist/consolidate/conflicts.js +3 -14
  128. package/dist/consolidate/decay.js +9 -29
  129. package/dist/consolidate/llm-passes.js +4 -5
  130. package/dist/consolidate/merge.js +8 -23
  131. package/dist/consolidate/run.d.ts +1 -8
  132. package/dist/consolidate/run.js +3 -25
  133. package/dist/consolidate/sleep.js +5 -17
  134. package/dist/consolidate/traces.js +9 -21
  135. package/dist/customer-notes.d.ts +5 -7
  136. package/dist/customer-notes.js +82 -76
  137. package/dist/dag.d.ts +10 -21
  138. package/dist/dag.js +189 -203
  139. package/dist/db/continuity.js +2 -2
  140. package/dist/db/migrations/v14.js +1 -1
  141. package/dist/db/migrations/v15.js +1 -2
  142. package/dist/db/migrations/v16.js +3 -4
  143. package/dist/db/migrations/v17.js +2 -3
  144. package/dist/db/migrations/v19.js +1 -1
  145. package/dist/db/migrations/v20.js +1 -1
  146. package/dist/db/migrations/v21.js +2 -6
  147. package/dist/db/migrations/v22.js +2 -4
  148. package/dist/db/migrations/v23.js +1 -1
  149. package/dist/db/migrations/v24.js +4 -6
  150. package/dist/db/migrations/v25.js +2 -3
  151. package/dist/db/migrations/v26.js +3 -3
  152. package/dist/db/migrations/v27.js +2 -10
  153. package/dist/db/migrations/v28.js +5 -8
  154. package/dist/db/migrations/v29.js +3 -4
  155. package/dist/db/migrations/v30.js +2 -2
  156. package/dist/db/migrations/v31.js +1 -1
  157. package/dist/db/migrations/v32.js +1 -1
  158. package/dist/db/migrations/v33.js +3 -3
  159. package/dist/db/migrations/v34.js +1 -1
  160. package/dist/db/migrations/v35.js +3 -4
  161. package/dist/db/migrations/v36.js +3 -4
  162. package/dist/db/migrations/v37.js +5 -5
  163. package/dist/db/migrations/v38.js +7 -8
  164. package/dist/db/migrations/v39.js +1 -1
  165. package/dist/db/migrations/v40.js +4 -16
  166. package/dist/db/migrations/v41.js +3 -4
  167. package/dist/db/migrations/v42.js +3 -4
  168. package/dist/db/migrations/v45.js +1 -1
  169. package/dist/db/migrations/v46.js +1 -1
  170. package/dist/db/migrations/v47.js +1 -1
  171. package/dist/db/migrations/v48.js +1 -1
  172. package/dist/decisions.d.ts +2 -2
  173. package/dist/decisions.js +97 -80
  174. package/dist/dedupe.js +86 -61
  175. package/dist/delivery-recorder.js +154 -135
  176. package/dist/doctor.js +129 -110
  177. package/dist/dormant.js +1 -4
  178. package/dist/embedding-provider.d.ts +4 -8
  179. package/dist/embedding-provider.js +4 -8
  180. package/dist/embeddings.js +55 -47
  181. package/dist/env.d.ts +1 -1
  182. package/dist/env.js +12 -12
  183. package/dist/escape.d.ts +5 -0
  184. package/dist/escape.js +10 -0
  185. package/dist/eval-stats.d.ts +1 -2
  186. package/dist/eval-stats.js +1 -2
  187. package/dist/eval-suite.js +27 -21
  188. package/dist/extract.js +4 -9
  189. package/dist/failure-log.d.ts +3 -3
  190. package/dist/failure-log.js +1 -1
  191. package/dist/forward-claim-detector.d.ts +2 -4
  192. package/dist/forward-claim-detector.js +6 -11
  193. package/dist/goals.d.ts +3 -3
  194. package/dist/goals.js +103 -91
  195. package/dist/graph/read.d.ts +2 -2
  196. package/dist/graph/read.js +5 -6
  197. package/dist/graph/types.d.ts +8 -8
  198. package/dist/graph/write.d.ts +7 -14
  199. package/dist/graph/write.js +16 -23
  200. package/dist/graph-extract.d.ts +7 -8
  201. package/dist/graph-extract.js +62 -72
  202. package/dist/graph-recall.d.ts +2 -2
  203. package/dist/graph-recall.js +55 -49
  204. package/dist/graph-stream.d.ts +5 -6
  205. package/dist/graph-stream.js +66 -57
  206. package/dist/graph-view.d.ts +2 -2
  207. package/dist/graph-view.js +7 -7
  208. package/dist/half-life-migration.d.ts +1 -2
  209. package/dist/half-life-migration.js +2 -3
  210. package/dist/hooks/codex-session.js +1 -1
  211. package/dist/hooks/codex-wrapper.d.ts +1 -1
  212. package/dist/hooks/codex-wrapper.js +3 -2
  213. package/dist/hooks/json-hooks.d.ts +2 -2
  214. package/dist/hooks/json-hooks.js +5 -4
  215. package/dist/hooks/opencode.d.ts +1 -1
  216. package/dist/hooks/opencode.js +5 -4
  217. package/dist/hooks/shared.d.ts +3 -7
  218. package/dist/hooks/shared.js +1 -8
  219. package/dist/http-util.d.ts +2 -3
  220. package/dist/http-util.js +3 -0
  221. package/dist/importers/core.d.ts +2 -9
  222. package/dist/importers/core.js +15 -30
  223. package/dist/importers/sources.js +2 -1
  224. package/dist/importers/vault.js +2 -20
  225. package/dist/incidents.d.ts +1 -1
  226. package/dist/incidents.js +46 -39
  227. package/dist/instruction-detect.d.ts +1 -1
  228. package/dist/instruction-detect.js +1 -1
  229. package/dist/invalidation.d.ts +3 -0
  230. package/dist/invalidation.js +160 -114
  231. package/dist/json.d.ts +5 -0
  232. package/dist/json.js +4 -0
  233. package/dist/judgment.js +1 -2
  234. package/dist/local-embedding.js +1 -1
  235. package/dist/mcp/admin-tools.js +7 -17
  236. package/dist/mcp/format.js +1 -1
  237. package/dist/mcp/framing.js +3 -6
  238. package/dist/mcp/protocol.d.ts +2 -5
  239. package/dist/mcp/protocol.js +1 -3
  240. package/dist/mcp/recall-tools.js +12 -15
  241. package/dist/mcp/request.js +4 -3
  242. package/dist/mcp/session-state.js +2 -3
  243. package/dist/mcp/stdio.js +2 -1
  244. package/dist/mcp/tools.js +9 -6
  245. package/dist/memory-value-weights.d.ts +5 -8
  246. package/dist/memory-value-weights.js +5 -8
  247. package/dist/memory-value.d.ts +13 -13
  248. package/dist/memory-value.js +26 -37
  249. package/dist/memory.d.ts +20 -22
  250. package/dist/memory.js +24 -48
  251. package/dist/multihop.d.ts +1 -1
  252. package/dist/multihop.js +3 -2
  253. package/dist/owner-validation.d.ts +4 -5
  254. package/dist/owner-validation.js +4 -5
  255. package/dist/physics.d.ts +4 -4
  256. package/dist/physics.js +7 -9
  257. package/dist/policies.d.ts +9 -10
  258. package/dist/policies.js +96 -81
  259. package/dist/postinstall.js +3 -6
  260. package/dist/predictions/planning-fallacy.d.ts +9 -14
  261. package/dist/predictions/planning-fallacy.js +10 -16
  262. package/dist/predictions/store.d.ts +15 -23
  263. package/dist/predictions/store.js +36 -33
  264. package/dist/processes.d.ts +2 -7
  265. package/dist/processes.js +88 -72
  266. package/dist/project-briefs.d.ts +2 -3
  267. package/dist/project-briefs.js +141 -118
  268. package/dist/project-identity.d.ts +22 -9
  269. package/dist/project-identity.js +47 -12
  270. package/dist/project-merge.d.ts +28 -5
  271. package/dist/project-merge.js +213 -46
  272. package/dist/project-remote.d.ts +12 -0
  273. package/dist/project-remote.js +138 -0
  274. package/dist/prompt-recall.js +1 -2
  275. package/dist/rate-limit.d.ts +1 -1
  276. package/dist/rate-limit.js +1 -1
  277. package/dist/raw-archive.d.ts +9 -0
  278. package/dist/raw-archive.js +70 -53
  279. package/dist/recall-history.d.ts +19 -20
  280. package/dist/recall-history.js +24 -42
  281. package/dist/recall-pipeline.js +4 -28
  282. package/dist/recall-scope.d.ts +7 -8
  283. package/dist/recall-scope.js +7 -8
  284. package/dist/recall-trace.d.ts +5 -9
  285. package/dist/recall-trace.js +6 -10
  286. package/dist/refine-llm.d.ts +1 -1
  287. package/dist/refine-llm.js +2 -2
  288. package/dist/reject-flow.d.ts +3 -4
  289. package/dist/reject-flow.js +122 -117
  290. package/dist/rejection.d.ts +5 -6
  291. package/dist/rejection.js +7 -15
  292. package/dist/rerankers/clef.d.ts +1 -1
  293. package/dist/rerankers/jev.d.ts +1 -2
  294. package/dist/rerankers/jev.js +4 -5
  295. package/dist/rerankers/llm.d.ts +1 -2
  296. package/dist/rerankers/llm.js +1 -2
  297. package/dist/rerankers/types.d.ts +1 -2
  298. package/dist/rrf.d.ts +2 -2
  299. package/dist/rrf.js +2 -2
  300. package/dist/search/bm25-search.d.ts +1 -1
  301. package/dist/search/bm25-search.js +2 -1
  302. package/dist/search/boosts.js +2 -1
  303. package/dist/search/hybrid.d.ts +1 -1
  304. package/dist/search/hybrid.js +2 -1
  305. package/dist/search/physics-search.d.ts +1 -1
  306. package/dist/search/physics-search.js +2 -1
  307. package/dist/search/types.d.ts +2 -0
  308. package/dist/search/types.js +3 -1
  309. package/dist/secret-detect.d.ts +4 -5
  310. package/dist/secret-detect.js +6 -10
  311. package/dist/server/auth.js +5 -5
  312. package/dist/server/client-ip.js +1 -1
  313. package/dist/server/cursor.js +2 -1
  314. package/dist/server/mcp-http.js +4 -4
  315. package/dist/server/request.d.ts +3 -6
  316. package/dist/server/request.js +6 -7
  317. package/dist/server/routes/admin.js +5 -4
  318. package/dist/server/routes/customer-notes.js +6 -5
  319. package/dist/server/routes/decisions.js +4 -3
  320. package/dist/server/routes/incidents.js +7 -5
  321. package/dist/server/routes/memories.js +7 -7
  322. package/dist/server/routes/policies.js +3 -2
  323. package/dist/server/routes/predictions.js +12 -15
  324. package/dist/server/routes/processes.js +3 -2
  325. package/dist/server/routes/project-briefs.js +8 -7
  326. package/dist/server/routes/recall.js +95 -93
  327. package/dist/server/routes/skills.js +6 -5
  328. package/dist/server/types.d.ts +1 -1
  329. package/dist/server/validation.d.ts +1 -2
  330. package/dist/server/validation.js +7 -14
  331. package/dist/server-detect.js +72 -58
  332. package/dist/server.d.ts +2 -2
  333. package/dist/server.js +131 -117
  334. package/dist/shared.d.ts +26 -17
  335. package/dist/shared.js +102 -104
  336. package/dist/skills.d.ts +3 -3
  337. package/dist/skills.js +88 -72
  338. package/dist/store/audit-event.d.ts +2 -2
  339. package/dist/store/audit-event.js +1 -1
  340. package/dist/store/candidates.d.ts +2 -2
  341. package/dist/store/candidates.js +4 -3
  342. package/dist/store/conflicts.js +30 -22
  343. package/dist/store/delete-and-batch.d.ts +11 -14
  344. package/dist/store/delete-and-batch.js +40 -91
  345. package/dist/store/entry-reads.d.ts +17 -25
  346. package/dist/store/entry-reads.js +59 -40
  347. package/dist/store/entry-row.d.ts +6 -24
  348. package/dist/store/entry-row.js +6 -24
  349. package/dist/store/entry-writes.d.ts +6 -7
  350. package/dist/store/entry-writes.js +13 -11
  351. package/dist/store/handoffs.d.ts +1 -1
  352. package/dist/store/handoffs.js +7 -10
  353. package/dist/store/index-and-stats.d.ts +2 -6
  354. package/dist/store/index-and-stats.js +4 -10
  355. package/dist/store/mirrors.d.ts +6 -19
  356. package/dist/store/mirrors.js +14 -39
  357. package/dist/store/open.js +9 -31
  358. package/dist/store/rows.d.ts +5 -11
  359. package/dist/store/rows.js +6 -11
  360. package/dist/store/search-rows.d.ts +17 -34
  361. package/dist/store/search-rows.js +34 -56
  362. package/dist/store/sessions.d.ts +4 -5
  363. package/dist/store/sessions.js +5 -6
  364. package/dist/store/summaries.d.ts +13 -17
  365. package/dist/store/summaries.js +26 -70
  366. package/dist/support-bundle.js +4 -8
  367. package/dist/tenant.d.ts +1 -5
  368. package/dist/token-ledger.d.ts +1 -1
  369. package/dist/token-ledger.js +3 -5
  370. package/dist/trace.js +1 -3
  371. package/dist/version.d.ts +1 -1
  372. package/dist/version.js +1 -1
  373. package/dist/working-memory.d.ts +1 -1
  374. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  375. package/extensions/openclaw-plugin/package.json +1 -1
  376. package/openclaw.plugin.json +1 -1
  377. package/package.json +1 -1
@@ -24,12 +24,7 @@ function byDepthThenScore(a, b) {
24
24
  return score;
25
25
  return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
26
26
  }
27
- /** A recorder for one call; every observer method is guarded, and a throw marks it broken instead of escaping. */
28
- export function createDeliveryRecorder(init) {
29
- const startedMs = Date.now();
30
- const ts = evalNow().toISOString();
31
- // Test-only fault injection, as HIPPO_FAKE_NOW is for time.
32
- const fault = envTestDeliveryFault();
27
+ function readPayload(init) {
33
28
  const payloadSession = hookPayloadSessionId(init.stdinText);
34
29
  const subagent = isSubagentPayload(init.stdinText);
35
30
  const envSession = init.envSessionId !== undefined && init.envSessionId !== '' ? init.envSessionId : null;
@@ -40,148 +35,127 @@ export function createDeliveryRecorder(init) {
40
35
  const sessionState = subagent
41
36
  ? 'subagent'
42
37
  : payloadSession !== null ? 'payload' : envSession !== null ? 'env' : 'missing';
43
- const candidates = new Map();
44
- const picked = new Map();
45
- const filtered = new Set();
46
- let facts = null;
47
- let shown = 0;
48
- let dropped = 0;
49
- let disabledSeen = false;
50
- let outcome = { state: 'empty' };
51
- let broken = null;
52
- let flushed = false;
53
- const guard = (fn) => {
54
- if (broken !== null)
55
- return;
56
- try {
57
- if (fault === 'observe')
58
- throw new Error('injected observe fault');
59
- fn();
60
- }
61
- catch (error) {
62
- broken = error instanceof Error ? error.message : String(error);
63
- }
64
- };
65
- const rejectId = (id, stage, reason, score, tokens) => {
66
- const held = candidates.get(id);
67
- // First rejection wins; an id never offered is not a candidate of this call.
68
- if (!held || held.reason !== null)
69
- return;
70
- candidates.set(id, { ...held, stage, reason, score, tokens });
71
- };
72
- const build = () => {
73
- if (fault === 'build')
74
- throw new Error('injected build fault');
75
- const staticReused = outcome.staticReused === true;
76
- const rows = [];
77
- for (const p of picked.values()) {
78
- const held = candidates.get(p.entry.id);
79
- rows.push({
80
- memoryId: p.entry.id,
81
- sourceStore: p.sourceStore,
82
- pool: p.promptRecall ? 'prompt-recall' : held?.pool === 'pin' || p.entry.pinned ? 'pin' : 'recent',
83
- stage: 'final',
84
- outcome: staticReused && !p.promptRecall ? 'reused' : 'emitted',
85
- reason: null,
86
- rank: p.rank,
87
- score: p.score,
88
- tokens: p.tokens,
89
- });
90
- }
91
- const rejected = [];
92
- let undecided = 0;
93
- for (const c of candidates.values()) {
94
- if (picked.has(c.id))
95
- continue;
96
- if (c.reason === null)
97
- undecided += 1;
98
- else
99
- rejected.push(c);
100
- }
101
- rejected.sort(byDepthThenScore);
102
- for (const c of rejected.slice(0, DELIVERY_REJECTED_ROW_CAP)) {
103
- rows.push({
104
- memoryId: c.id, sourceStore: c.sourceStore, pool: c.pool, stage: c.stage ?? 'load', outcome: 'rejected',
105
- reason: c.reason, rank: null, score: c.score, tokens: c.tokens,
106
- });
107
- }
108
- const overflow = Math.max(0, rejected.length - DELIVERY_REJECTED_ROW_CAP);
109
- const emitted = outcome.emittedText ?? null;
110
- return {
111
- ts,
112
- tenantId: init.tenantId,
113
- runtime: hostTurnId !== null ? 'codex' : hookEvent !== null ? 'claude-code' : 'unknown',
114
- eventType: hookEvent === 'UserPromptSubmit' ? 'prompt-submit' : 'pinned-manual',
115
- surface: 'hook',
116
- storeHash: init.storeHash,
117
- writeStore: init.writeStore,
118
- projectHash: facts !== null && facts.projectName !== '' ? blockHash(facts.projectName) : null,
119
- sessionId: payloadSession ?? envSession,
120
- sessionState,
121
- hostTurnId,
122
- promptHash: prompt !== null ? blockHash(prompt) : null,
123
- promptLength: prompt?.length ?? 0,
124
- queryHash: null,
125
- recallTraceId: null,
126
- blockState: disabledSeen ? 'disabled' : outcome.state,
127
- promptRecall: facts?.promptRecall === true,
128
- consideredCount: new Set([...candidates.keys(), ...picked.keys()]).size,
129
- filteredCount: filtered.size,
130
- selectedCount: picked.size,
131
- emittedCount: rows.filter((r) => r.outcome === 'emitted').length,
132
- rejectedCount: rejected.length + undecided,
133
- rejectedUnlisted: overflow + undecided,
134
- sectionsShown: shown,
135
- sectionsDropped: dropped,
136
- budgetTokens: facts?.budgetTokens ?? 0,
137
- selectedTokens: [...picked.values()].reduce((sum, p) => sum + p.tokens, 0),
138
- injectedTokens: emitted !== null ? estimateTokens(emitted) : 0,
139
- staticHash: outcome.staticHash ?? null,
140
- recallHash: outcome.recallHash ?? null,
141
- emittedHash: emitted !== null ? blockHash(emitted) : null,
142
- elapsedMs: Math.max(0, Date.now() - startedMs),
143
- candidates: rows,
144
- };
38
+ return { payloadSession, envSession, prompt, hostTurnId, hookEvent, sessionState };
39
+ }
40
+ function rejectId(state, id, stage, reason, score, tokens) {
41
+ const held = state.candidates.get(id);
42
+ // First rejection wins; an id never offered is not a candidate of this call.
43
+ if (!held || held.reason !== null)
44
+ return;
45
+ state.candidates.set(id, { ...held, stage, reason, score, tokens });
46
+ }
47
+ function pickedRows(state) {
48
+ const staticReused = state.outcome.staticReused === true;
49
+ const rows = [];
50
+ for (const p of state.picked.values()) {
51
+ const held = state.candidates.get(p.entry.id);
52
+ rows.push({
53
+ memoryId: p.entry.id,
54
+ sourceStore: p.sourceStore,
55
+ pool: p.promptRecall ? 'prompt-recall' : held?.pool === 'pin' || p.entry.pinned ? 'pin' : 'recent',
56
+ stage: 'final',
57
+ outcome: staticReused && !p.promptRecall ? 'reused' : 'emitted',
58
+ reason: null,
59
+ rank: p.rank,
60
+ score: p.score,
61
+ tokens: p.tokens,
62
+ });
63
+ }
64
+ return rows;
65
+ }
66
+ /** Rejected candidates deepest first, capped at DELIVERY_REJECTED_ROW_CAP rows. */
67
+ function rejectedRows(state) {
68
+ const rejected = [];
69
+ let undecided = 0;
70
+ for (const c of state.candidates.values()) {
71
+ if (state.picked.has(c.id))
72
+ continue;
73
+ if (c.reason === null)
74
+ undecided += 1;
75
+ else
76
+ rejected.push(c);
77
+ }
78
+ rejected.sort(byDepthThenScore);
79
+ const rows = rejected.slice(0, DELIVERY_REJECTED_ROW_CAP).map((c) => ({
80
+ memoryId: c.id, sourceStore: c.sourceStore, pool: c.pool, stage: c.stage ?? 'load', outcome: 'rejected',
81
+ reason: c.reason, rank: null, score: c.score, tokens: c.tokens,
82
+ }));
83
+ const overflow = Math.max(0, rejected.length - DELIVERY_REJECTED_ROW_CAP);
84
+ return { rows, rejected: rejected.length, undecided, overflow };
85
+ }
86
+ function buildEvent(init, payload, state, ts, startedMs) {
87
+ const { facts, outcome } = state;
88
+ const picked = pickedRows(state);
89
+ const rejected = rejectedRows(state);
90
+ const rows = [...picked, ...rejected.rows];
91
+ const emitted = outcome.emittedText ?? null;
92
+ return {
93
+ ts,
94
+ tenantId: init.tenantId,
95
+ runtime: payload.hostTurnId !== null ? 'codex' : payload.hookEvent !== null ? 'claude-code' : 'unknown',
96
+ eventType: payload.hookEvent === 'UserPromptSubmit' ? 'prompt-submit' : 'pinned-manual',
97
+ surface: 'hook',
98
+ storeHash: init.storeHash,
99
+ writeStore: init.writeStore,
100
+ projectHash: facts !== null && facts.projectName !== '' ? blockHash(facts.projectName) : null,
101
+ sessionId: payload.payloadSession ?? payload.envSession,
102
+ sessionState: payload.sessionState,
103
+ hostTurnId: payload.hostTurnId,
104
+ promptHash: payload.prompt !== null ? blockHash(payload.prompt) : null,
105
+ promptLength: payload.prompt?.length ?? 0,
106
+ queryHash: null,
107
+ recallTraceId: null,
108
+ blockState: state.disabledSeen ? 'disabled' : outcome.state,
109
+ promptRecall: facts?.promptRecall === true,
110
+ consideredCount: new Set([...state.candidates.keys(), ...state.picked.keys()]).size,
111
+ filteredCount: state.filtered.size,
112
+ selectedCount: state.picked.size,
113
+ emittedCount: rows.filter((r) => r.outcome === 'emitted').length,
114
+ rejectedCount: rejected.rejected + rejected.undecided,
115
+ rejectedUnlisted: rejected.overflow + rejected.undecided,
116
+ sectionsShown: state.shown,
117
+ sectionsDropped: state.dropped,
118
+ budgetTokens: facts?.budgetTokens ?? 0,
119
+ selectedTokens: [...state.picked.values()].reduce((sum, p) => sum + p.tokens, 0),
120
+ injectedTokens: emitted !== null ? estimateTokens(emitted) : 0,
121
+ staticHash: outcome.staticHash ?? null,
122
+ recallHash: outcome.recallHash ?? null,
123
+ emittedHash: emitted !== null ? blockHash(emitted) : null,
124
+ elapsedMs: Math.max(0, Date.now() - startedMs),
125
+ candidates: rows,
145
126
  };
127
+ }
128
+ /** The candidate-tracking half of the observer, each method run through `guard`. */
129
+ function candidateMethods(state, guard) {
146
130
  return {
147
- root: init.root,
148
- facts: (f) => guard(() => { facts = { ...f }; }),
149
- sections: (s, d) => guard(() => { shown = s; dropped = d; }),
150
- watchAdmit: (admit) => (e) => {
151
- const ok = admit(e);
152
- if (!ok)
153
- guard(() => { filtered.add(e.id); });
154
- return ok;
155
- },
156
131
  qualityDropped: (e, isGlobal) => guard(() => {
157
- filtered.add(e.id);
158
- if (!candidates.has(e.id)) {
159
- candidates.set(e.id, {
132
+ state.filtered.add(e.id);
133
+ if (!state.candidates.has(e.id)) {
134
+ state.candidates.set(e.id, {
160
135
  id: e.id, sourceStore: storeOf(isGlobal), pool: 'recent', stage: null, reason: null, score: null, tokens: null,
161
136
  });
162
137
  }
163
- rejectId(e.id, 'load', 'quality', null, null);
138
+ rejectId(state, e.id, 'load', 'quality', null, null);
164
139
  }),
165
- disabled: () => guard(() => { disabledSeen = true; }),
166
140
  offer: (entries, isGlobal, pool) => guard(() => {
167
141
  for (const e of entries) {
168
- const held = candidates.get(e.id);
142
+ const held = state.candidates.get(e.id);
169
143
  const wanted = pool ?? (e.pinned ? 'pin' : 'recent');
170
144
  // A loaded recent row the prompt-recall gate then judges belongs to that pool.
171
145
  const relabel = held !== undefined && held.reason === null && held.pool === 'recent' && wanted === 'prompt-recall';
172
146
  if (held !== undefined && !relabel)
173
147
  continue;
174
- candidates.set(e.id, {
148
+ state.candidates.set(e.id, {
175
149
  id: e.id, sourceStore: storeOf(isGlobal), pool: wanted, stage: null, reason: null, score: null, tokens: null,
176
150
  });
177
151
  }
178
152
  }),
179
- reject: (e, stage, reason, score, tokens) => guard(() => rejectId(e.id, stage, reason, score ?? null, tokens ?? null)),
153
+ reject: (e, stage, reason, score, tokens) => guard(() => rejectId(state, e.id, stage, reason, score ?? null, tokens ?? null)),
180
154
  dropMissing: (before, after, stage, reason) => guard(() => {
181
155
  const kept = new Set(after.map((e) => e.id));
182
156
  for (const e of before)
183
157
  if (!kept.has(e.id))
184
- rejectId(e.id, stage, reason, null, null);
158
+ rejectId(state, e.id, stage, reason, null, null);
185
159
  }),
186
160
  gated: (prompt, items, gate, kept) => guard(() => {
187
161
  const keptIds = new Set(kept.map((g) => g.item.id));
@@ -190,29 +164,74 @@ export function createDeliveryRecorder(init) {
190
164
  continue;
191
165
  const { score, shared } = scoreOverlap(prompt, c.tokens, gate.metric);
192
166
  const cleared = score >= gate.threshold && shared >= gate.minShared;
193
- rejectId(c.id, 'gate', cleared ? 'gate-max-items' : 'gate-below-threshold', score, null);
167
+ rejectId(state, c.id, 'gate', cleared ? 'gate-max-items' : 'gate-below-threshold', score, null);
194
168
  }
195
169
  }),
196
170
  selected: (items) => guard(() => {
197
- picked.clear();
171
+ state.picked.clear();
198
172
  items.forEach((r, i) => {
199
- picked.set(r.entry.id, {
173
+ state.picked.set(r.entry.id, {
200
174
  entry: r.entry, rank: i + 1, score: r.score, tokens: r.tokens,
201
175
  sourceStore: storeOf(r.isGlobal), promptRecall: r.promptRecall === true,
202
176
  });
203
177
  });
204
178
  }),
205
- delivered: (o) => guard(() => { outcome = { ...o }; }),
179
+ };
180
+ }
181
+ /** A recorder for one call; every observer method is guarded, and a throw marks it broken instead of escaping. */
182
+ export function createDeliveryRecorder(init) {
183
+ const startedMs = Date.now();
184
+ const ts = evalNow().toISOString();
185
+ // Test-only fault injection, as HIPPO_FAKE_NOW is for time.
186
+ const fault = envTestDeliveryFault();
187
+ const payload = readPayload(init);
188
+ const state = {
189
+ candidates: new Map(), picked: new Map(), filtered: new Set(), facts: null, shown: 0, dropped: 0,
190
+ disabledSeen: false, outcome: { state: 'empty' }, broken: null, flushed: false,
191
+ };
192
+ const guard = (fn) => {
193
+ if (state.broken !== null)
194
+ return;
195
+ try {
196
+ if (fault === 'observe')
197
+ throw new Error('injected observe fault');
198
+ fn();
199
+ }
200
+ catch (error) {
201
+ state.broken = error instanceof Error ? error.message : String(error);
202
+ }
203
+ };
204
+ const { qualityDropped, offer, reject, dropMissing, gated, selected } = candidateMethods(state, guard);
205
+ return {
206
+ root: init.root,
207
+ facts: (f) => guard(() => { state.facts = { ...f }; }),
208
+ sections: (s, d) => guard(() => { state.shown = s; state.dropped = d; }),
209
+ watchAdmit: (admit) => (e) => {
210
+ const ok = admit(e);
211
+ if (!ok)
212
+ guard(() => { state.filtered.add(e.id); });
213
+ return ok;
214
+ },
215
+ qualityDropped,
216
+ disabled: () => guard(() => { state.disabledSeen = true; }),
217
+ offer,
218
+ reject,
219
+ dropMissing,
220
+ gated,
221
+ selected,
222
+ delivered: (o) => guard(() => { state.outcome = { ...o }; }),
206
223
  flush: (write) => {
207
- if (flushed)
224
+ if (state.flushed)
208
225
  return;
209
- flushed = true;
210
- if (broken !== null) {
226
+ state.flushed = true;
227
+ if (state.broken !== null) {
211
228
  // Same pinned `[hippo] delivery ledger` hook stderr line as recall-trace.ts's write failure.
212
- console.error(`[hippo] delivery ledger skipped: recorder failed: ${broken}`);
229
+ console.error(`[hippo] delivery ledger skipped: recorder failed: ${state.broken}`);
213
230
  return;
214
231
  }
215
- const input = build();
232
+ if (fault === 'build')
233
+ throw new Error('injected build fault');
234
+ const input = buildEvent(init, payload, state, ts, startedMs);
216
235
  if (fault === 'flush')
217
236
  throw new Error('injected flush fault');
218
237
  write(input);
package/dist/doctor.js CHANGED
@@ -15,7 +15,7 @@ import { openHippoDbReadOnly, closeHippoDb, getSchemaVersion, getCurrentSchemaVe
15
15
  import { REPLAY_AFTER_MS, TRANSCRIPT_FILL_WINDOW_MS } from './compaction-record.js';
16
16
  import { isEmbeddingAvailable } from './local-embedding.js';
17
17
  import { CODEX_TRUST_LINE, codexHomeDir, isCodexPresent, isJsonObject } from './hooks/shared.js';
18
- import { planUserGlobalRepair } from './project-merge.js';
18
+ import { planProjectRepair } from './project-merge.js';
19
19
  import { resolveTenantId } from './tenant.js';
20
20
  /** Minimum Node.js version hippo supports (package.json engines). */
21
21
  export const MIN_NODE = '22.16.0';
@@ -131,16 +131,21 @@ function compactionsCheck(db, now) {
131
131
  : { id: 'compactions', status: 'warn', detail: `cannot read the compaction records: ${message}` };
132
132
  }
133
133
  }
134
- /** Merged rows older sleep saved as user-global, counted by the repair's own dry run so a truly user-global merge never warns. */
134
+ /** Old project tags in the global store, counted by the repair's own plan so a truly user-global merge never warns. */
135
135
  function projectsCheck(globalRoot) {
136
136
  let db = null;
137
137
  try {
138
138
  db = openHippoDbReadOnly(globalRoot);
139
- const r = planUserGlobalRepair(db, resolveTenantId({}));
140
- const n = r.toProject.length + r.setAside.length;
141
- return n === 0
142
- ? { id: 'projects', status: 'pass', detail: 'no merged memories in the global store are tagged user-global by mistake' }
143
- : { id: 'projects', status: 'warn', detail: `${n} merged memories in the global store are tagged user-global, so every project's context can show them`, fix: 'hippo projects repair --global (dry run; add --apply to write)' };
139
+ const r = planProjectRepair(db, globalRoot, resolveTenantId({}));
140
+ const found = [
141
+ r.copies.length > 0 ? `${r.copies.length} imported notes copied under the wrong project` : '',
142
+ r.folds.length > 0 ? `old project names that now resolve to another project: ${r.folds.map((f) => `${f.from} -> ${f.into}`).join(', ')}` : '',
143
+ r.collisions.length > 0 ? `old project names shared by several projects, folded by hand only: ${r.collisions.map((c) => `${c.name} (${c.ids.join(', ')})`).join('; ')}` : '',
144
+ r.toProject.length + r.setAside.length > 0 ? `${r.toProject.length + r.setAside.length} merged memories tagged user-global` : '',
145
+ ].filter((s) => s !== '');
146
+ return found.length === 0
147
+ ? { id: 'projects', status: 'pass', detail: 'no duplicate or out-of-date project tags in the global store' }
148
+ : { id: 'projects', status: 'warn', detail: `global store: ${found.join('; ')}`, fix: 'hippo projects repair --global (dry run; add --apply to write)' };
144
149
  }
145
150
  catch (err) {
146
151
  return { id: 'projects', status: 'info', detail: `project tags not checked (${err instanceof Error ? err.message : String(err)})` };
@@ -150,124 +155,138 @@ function projectsCheck(globalRoot) {
150
155
  closeHippoDb(db);
151
156
  }
152
157
  }
158
+ function nodeCheck(node) {
159
+ return versionAtLeast(node, MIN_NODE)
160
+ ? { id: 'node', status: 'pass', detail: `Node.js ${node}` }
161
+ : { id: 'node', status: 'fail', detail: `Node.js ${node} is older than ${MIN_NODE}`, fix: `Install Node.js ${MIN_NODE} or newer` };
162
+ }
163
+ /** The store the checks run against (project first, then global), and the check that says which. */
164
+ function storeCheck(local, globalRoot, hasGlobal) {
165
+ if (local !== null && isInitialized(local)) {
166
+ return { store: local, check: { id: 'store', status: 'pass', detail: `project store at ${local}${hasGlobal ? ` (global store at ${globalRoot} too)` : ''}` } };
167
+ }
168
+ if (local !== null) {
169
+ // The walk stops at the first .hippo it finds, so a bare one (no hippo.db) blocks a parent or global store too.
170
+ return { store: null, check: { id: 'store', status: 'fail', detail: `${local} has no hippo.db, so hippo commands run here stop at it`, fix: `run hippo init in ${path.dirname(local)}, or remove that .hippo folder` } };
171
+ }
172
+ if (hasGlobal) {
173
+ return { store: globalRoot, check: { id: 'store', status: 'warn', detail: `no project store here; using the global store at ${globalRoot}`, fix: 'hippo init (in the project root)' } };
174
+ }
175
+ return { store: null, check: { id: 'store', status: 'fail', detail: 'no hippo store found (project or global)', fix: 'hippo init (in the project root), or hippo init --global' } };
176
+ }
177
+ function schemaCheck(have, want) {
178
+ return have === want
179
+ ? { id: 'schema', status: 'pass', detail: `database schema v${have}` }
180
+ : have > want
181
+ ? { id: 'schema', status: 'fail', detail: `database schema v${have} is newer than this hippo (v${want})`, fix: 'npm install -g hippo-memory@latest' }
182
+ : { id: 'schema', status: 'info', detail: `database schema v${have}; hippo migrates it to v${want} on the next write` };
183
+ }
184
+ function memoriesCheck(db) {
185
+ const memories = countTableRows(db, 'memories');
186
+ const dormant = countTableRows(db, 'dormant_memories');
187
+ const memoryCheck = { id: 'memories', status: 'info', detail: `${memories ?? '?'} memories${dormant !== null ? `, ${dormant} dormant` : ''}` };
188
+ if (memories === 0) {
189
+ memoryCheck.status = 'warn';
190
+ memoryCheck.fix = 'hippo learn --git (seed lessons from git history)';
191
+ }
192
+ return memoryCheck;
193
+ }
194
+ function tokensCheck(db, since) {
195
+ try {
196
+ // SAFETY: COUNT/SUM aggregate row.
197
+ const row = db.prepare(`SELECT COUNT(CASE WHEN event = 'inject' THEN 1 END) AS n,
198
+ COALESCE(SUM(CASE WHEN event = 'inject' THEN tokens END), 0) AS t,
199
+ COALESCE(SUM(CASE WHEN event = 'reread' THEN tokens END), 0) AS r
200
+ FROM token_ledger WHERE ts >= ?`).get(since);
201
+ return {
202
+ id: 'tokens',
203
+ status: 'info',
204
+ detail: `${Number(row?.n ?? 0)} memory blocks sent to agents in 7 days, about ${Number(row?.t ?? 0)} tokens sent and ${Number(row?.r ?? 0)} re-read by later model calls (hippo tokens for detail)`,
205
+ };
206
+ }
207
+ catch {
208
+ return { id: 'tokens', status: 'info', detail: 'no token ledger yet (created on the next write)' };
209
+ }
210
+ }
211
+ /** Checks that read the store's database; a failed open becomes a failed schema check after whatever already passed. */
212
+ function databaseChecks(store, now) {
213
+ const checks = [];
214
+ let db = null;
215
+ try {
216
+ db = openHippoDbReadOnly(store);
217
+ const have = getSchemaVersion(db);
218
+ checks.push(schemaCheck(have, getCurrentSchemaVersion()));
219
+ checks.push(memoriesCheck(db));
220
+ const since = new Date(now.getTime() - 7 * 86_400_000).toISOString();
221
+ checks.push(tokensCheck(db, since));
222
+ checks.push(failuresCheck(db, since, have));
223
+ checks.push(sleepCheck(db, now));
224
+ checks.push(compactionsCheck(db, now));
225
+ }
226
+ catch (err) {
227
+ checks.push({
228
+ id: 'schema',
229
+ status: 'fail',
230
+ detail: `cannot open the database: ${err instanceof Error ? err.message : String(err)}`,
231
+ fix: err instanceof IncompatibleBinaryError ? 'npm install -g hippo-memory@latest' : 'check file permissions on the .hippo folder',
232
+ });
233
+ }
234
+ finally {
235
+ if (db !== null)
236
+ closeHippoDb(db);
237
+ }
238
+ return checks;
239
+ }
240
+ // Each hook and what it does, so a partial install says what is missing.
241
+ const CLAUDE_CODE_HOOKS = [
242
+ ['hippo context --pinned-only', 'per-prompt memory'],
243
+ ['hippo session-end', 'session-end capture and sleep'],
244
+ ['hippo pre-compact', 'compaction snapshot and memories request'],
245
+ ['hippo compact-resume', 'resume after compaction'],
246
+ ['hippo post-compact', 'saving the memories a compaction lists'],
247
+ ['hippo capture-error', 'failed-tool capture'],
248
+ ];
249
+ function claudeCodeCheck(home) {
250
+ const claudeDir = path.join(home, '.claude');
251
+ if (!fs.existsSync(claudeDir)) {
252
+ return { id: 'claude-code', status: 'info', detail: 'Claude Code not found; other agents can use hippo over MCP (hippo mcp)' };
253
+ }
254
+ const settings = readJson(path.join(claudeDir, 'settings.json'));
255
+ const text = settings === null ? '' : JSON.stringify(settings);
256
+ const viaPlugin = /hippo-memory@/.test(text);
257
+ const missing = CLAUDE_CODE_HOOKS.filter(([marker]) => !text.includes(marker)).map(([, what]) => what);
258
+ if (viaPlugin) {
259
+ return { id: 'claude-code', status: 'pass', detail: 'Claude Code: hippo plugin enabled (all hooks, including compaction and failed-tool capture)' };
260
+ }
261
+ if (missing.length === 0) {
262
+ return { id: 'claude-code', status: 'pass', detail: 'Claude Code: all hippo hooks installed, including compaction and failed-tool capture' };
263
+ }
264
+ if (missing.length === CLAUDE_CODE_HOOKS.length) {
265
+ return { id: 'claude-code', status: 'warn', detail: 'Claude Code found, but no hippo hooks are installed', fix: 'hippo hook install claude-code' };
266
+ }
267
+ return { id: 'claude-code', status: 'warn', detail: `Claude Code: hippo hooks missing for ${missing.join(', ')}`, fix: 'hippo hook install claude-code (adds only what is missing)' };
268
+ }
153
269
  /** Run every check. Never throws for a broken install; broken parts become failed checks. */
154
270
  export function runDoctor(opts) {
155
271
  const cwd = opts.cwd ?? process.cwd();
156
272
  const home = opts.home ?? os.homedir();
157
273
  const now = opts.now ?? new Date();
158
274
  const checks = [];
159
- const node = opts.nodeVersion ?? process.versions.node;
160
- checks.push(versionAtLeast(node, MIN_NODE)
161
- ? { id: 'node', status: 'pass', detail: `Node.js ${node}` }
162
- : { id: 'node', status: 'fail', detail: `Node.js ${node} is older than ${MIN_NODE}`, fix: `Install Node.js ${MIN_NODE} or newer` });
275
+ checks.push(nodeCheck(opts.nodeVersion ?? process.versions.node));
163
276
  const local = findHippoStoreDir(cwd);
164
277
  const globalRoot = getGlobalRoot();
165
278
  const hasGlobal = isInitialized(globalRoot);
166
- let store = null;
167
- if (local !== null && isInitialized(local)) {
168
- store = local;
169
- checks.push({ id: 'store', status: 'pass', detail: `project store at ${local}${hasGlobal ? ` (global store at ${globalRoot} too)` : ''}` });
170
- }
171
- else if (local !== null) {
172
- // The walk stops at the first .hippo it finds, so a bare one (no hippo.db) blocks a parent or global store too.
173
- checks.push({ id: 'store', status: 'fail', detail: `${local} has no hippo.db, so hippo commands run here stop at it`, fix: `run hippo init in ${path.dirname(local)}, or remove that .hippo folder` });
174
- }
175
- else if (hasGlobal) {
176
- store = globalRoot;
177
- checks.push({ id: 'store', status: 'warn', detail: `no project store here; using the global store at ${globalRoot}`, fix: 'hippo init (in the project root)' });
178
- }
179
- else {
180
- checks.push({ id: 'store', status: 'fail', detail: 'no hippo store found (project or global)', fix: 'hippo init (in the project root), or hippo init --global' });
181
- }
182
- if (store !== null) {
183
- let db = null;
184
- try {
185
- db = openHippoDbReadOnly(store);
186
- const have = getSchemaVersion(db);
187
- const want = getCurrentSchemaVersion();
188
- checks.push(have === want
189
- ? { id: 'schema', status: 'pass', detail: `database schema v${have}` }
190
- : have > want
191
- ? { id: 'schema', status: 'fail', detail: `database schema v${have} is newer than this hippo (v${want})`, fix: 'npm install -g hippo-memory@latest' }
192
- : { id: 'schema', status: 'info', detail: `database schema v${have}; hippo migrates it to v${want} on the next write` });
193
- const memories = countTableRows(db, 'memories');
194
- const dormant = countTableRows(db, 'dormant_memories');
195
- const memoryCheck = { id: 'memories', status: 'info', detail: `${memories ?? '?'} memories${dormant !== null ? `, ${dormant} dormant` : ''}` };
196
- if (memories === 0) {
197
- memoryCheck.status = 'warn';
198
- memoryCheck.fix = 'hippo learn --git (seed lessons from git history)';
199
- }
200
- checks.push(memoryCheck);
201
- const since = new Date(now.getTime() - 7 * 86_400_000).toISOString();
202
- try {
203
- // SAFETY: COUNT/SUM aggregate row.
204
- const row = db.prepare(`SELECT COUNT(CASE WHEN event = 'inject' THEN 1 END) AS n,
205
- COALESCE(SUM(CASE WHEN event = 'inject' THEN tokens END), 0) AS t,
206
- COALESCE(SUM(CASE WHEN event = 'reread' THEN tokens END), 0) AS r
207
- FROM token_ledger WHERE ts >= ?`).get(since);
208
- checks.push({
209
- id: 'tokens',
210
- status: 'info',
211
- detail: `${Number(row?.n ?? 0)} memory blocks sent to agents in 7 days, about ${Number(row?.t ?? 0)} tokens sent and ${Number(row?.r ?? 0)} re-read by later model calls (hippo tokens for detail)`,
212
- });
213
- }
214
- catch {
215
- checks.push({ id: 'tokens', status: 'info', detail: 'no token ledger yet (created on the next write)' });
216
- }
217
- checks.push(failuresCheck(db, since, have));
218
- checks.push(sleepCheck(db, now));
219
- checks.push(compactionsCheck(db, now));
220
- }
221
- catch (err) {
222
- checks.push({
223
- id: 'schema',
224
- status: 'fail',
225
- detail: `cannot open the database: ${err instanceof Error ? err.message : String(err)}`,
226
- fix: err instanceof IncompatibleBinaryError ? 'npm install -g hippo-memory@latest' : 'check file permissions on the .hippo folder',
227
- });
228
- }
229
- finally {
230
- if (db !== null)
231
- closeHippoDb(db);
232
- }
233
- }
279
+ const { store, check } = storeCheck(local, globalRoot, hasGlobal);
280
+ checks.push(check);
281
+ if (store !== null)
282
+ checks.push(...databaseChecks(store, now));
234
283
  const holdoutRateBp = store === null ? 0 : loadConfig(store).pilot.holdoutRateBp;
235
284
  if (holdoutRateBp > 0) {
236
285
  checks.push({ id: 'pilot', status: 'info', detail: `pilot holdout on: about ${holdoutRateBp / 100}% of sessions get no memories pushed by hippo (pilot.holdoutRateBp=${holdoutRateBp})` });
237
286
  }
238
287
  if (hasGlobal)
239
288
  checks.push(projectsCheck(globalRoot));
240
- const claudeDir = path.join(home, '.claude');
241
- if (fs.existsSync(claudeDir)) {
242
- const settings = readJson(path.join(claudeDir, 'settings.json'));
243
- const text = settings === null ? '' : JSON.stringify(settings);
244
- const viaPlugin = /hippo-memory@/.test(text);
245
- // Each hook and what it does, so a partial install says what is missing.
246
- const hooks = [
247
- ['hippo context --pinned-only', 'per-prompt memory'],
248
- ['hippo session-end', 'session-end capture and sleep'],
249
- ['hippo pre-compact', 'compaction snapshot and memories request'],
250
- ['hippo compact-resume', 'resume after compaction'],
251
- ['hippo post-compact', 'saving the memories a compaction lists'],
252
- ['hippo capture-error', 'failed-tool capture'],
253
- ];
254
- const missing = hooks.filter(([marker]) => !text.includes(marker)).map(([, what]) => what);
255
- if (viaPlugin) {
256
- checks.push({ id: 'claude-code', status: 'pass', detail: 'Claude Code: hippo plugin enabled (all hooks, including compaction and failed-tool capture)' });
257
- }
258
- else if (missing.length === 0) {
259
- checks.push({ id: 'claude-code', status: 'pass', detail: 'Claude Code: all hippo hooks installed, including compaction and failed-tool capture' });
260
- }
261
- else if (missing.length === hooks.length) {
262
- checks.push({ id: 'claude-code', status: 'warn', detail: 'Claude Code found, but no hippo hooks are installed', fix: 'hippo hook install claude-code' });
263
- }
264
- else {
265
- checks.push({ id: 'claude-code', status: 'warn', detail: `Claude Code: hippo hooks missing for ${missing.join(', ')}`, fix: 'hippo hook install claude-code (adds only what is missing)' });
266
- }
267
- }
268
- else {
269
- checks.push({ id: 'claude-code', status: 'info', detail: 'Claude Code not found; other agents can use hippo over MCP (hippo mcp)' });
270
- }
289
+ checks.push(claudeCodeCheck(home));
271
290
  if (isCodexPresent(home))
272
291
  checks.push(codexCheck(home));
273
292
  checks.push({ id: 'embeddings', status: 'info', detail: isEmbeddingAvailable() ? 'local embeddings available (hybrid search)' : 'embeddings not installed; recall uses BM25 (optional: hippo embed --help)' });
package/dist/dormant.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { rejectionDigest } from './rejection.js';
2
+ import { escapeLike } from './escape.js';
2
3
  const DEFAULT_LIST_LIMIT = 20;
3
4
  /**
4
5
  * Insert (or refresh) the dormant snapshot for `move.entry`. Does not touch
@@ -17,10 +18,6 @@ export function insertDormantRow(db, move) {
17
18
  dormant_at = excluded.dormant_at
18
19
  `).run(move.entry.tenantId, move.entry.id, move.entry.content, JSON.stringify(move.entry), move.reason, move.strength, move.dormantAt);
19
20
  }
20
- /** Escape LIKE metacharacters so a search term matches literally (ESCAPE '\'). */
21
- function escapeLike(term) {
22
- return term.replace(/[\\%_]/g, (ch) => `\\${ch}`);
23
- }
24
21
  /**
25
22
  * Parse a stored snapshot back into a MemoryEntry, or null when the row no
26
23
  * longer matches the snapshot it carries (edited by hand, or truncated).