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
package/dist/server.js CHANGED
@@ -61,7 +61,6 @@ function isAddressInfo(a) {
61
61
  // HTTP /health response uses this; reading package.json synchronously here
62
62
  // would couple the daemon to its on-disk install path, which we want to
63
63
  // avoid for tests that mkdtemp a hippoRoot.
64
- // v1.3.1: source from src/version.ts so /health no longer reports stale 0.39.0.
65
64
  const VERSION = PACKAGE_VERSION;
66
65
  const LOOPBACK_HOSTS = new Set(['127.0.0.1', '::1', 'localhost']);
67
66
  /** The /v1 routes in dispatch order; the first entry whose method and path match handles the request. */
@@ -159,7 +158,7 @@ async function dispatchV1Route(r, method, path) {
159
158
  return false;
160
159
  }
161
160
  async function handleRequest(req, res, opts, startedAt, streamSlots, limiter) {
162
- // v1.6.4: pre-decode raw-URL slash check. Catches `%2F` / `%2f` before
161
+ // Pre-decode raw-URL slash check. Catches `%2F` / `%2f` before
163
162
  // Node's URL parser collapses them and they slip past the route table.
164
163
  rejectEncodedSlash(req.url ?? '/');
165
164
  const { method, path, query } = parseRequest(req);
@@ -215,126 +214,93 @@ function sendHealth(req, res, startedAt) {
215
214
  sendJson(res, 200, { ok: true });
216
215
  }
217
216
  }
218
- /**
219
- * Boot the HTTP daemon on host:port and write the pidfile under hippoRoot.
220
- *
221
- * Refuses non-loopback hosts at boot (Footgun #3 from the A1 plan) unless
222
- * HIPPO_REQUIRE_AUTH=1 is set. The A5 v2 auth middleware (buildContextWithAuth /
223
- * requireAuth) has shipped and every route checks it except GET /health
224
- * (public by design for platform health checks) and the two connector
225
- * webhooks in PUBLIC_ROUTES, which are HMAC-gated by their own signing
226
- * secrets and 404 when those secrets are unset. But the loopback
227
- * no-auth fallback inside buildContextWithAuth still admits unauthenticated
228
- * requests from a loopback remote address (unless they carry Forwarded,
229
- * X-Forwarded-For/-Host/-Proto, X-Real-IP, Cf-Connecting-Ip or True-Client-Ip, which mark a same-host proxy and get
230
- * a 401 like any keyless remote request), so binding to a non-loopback host
231
- * is only safe once that fallback is disabled with HIPPO_REQUIRE_AUTH=1,
232
- * which forces every request (loopback or not) through Bearer-token
233
- * validation. Without that env var set, a non-loopback bind would expose the
234
- * DB to the network with no auth, so we fail fast instead.
235
- *
236
- * Use port: 0 in tests to bind to an ephemeral port and read the actual
237
- * port back via server.address() after listen.
238
- */
239
- export async function serve(opts) {
240
- const host = opts.host ?? '127.0.0.1';
241
- const requestedPort = opts.port ?? Number(envPort() ?? 6789);
217
+ function assertBindable(host) {
242
218
  if (!LOOPBACK_HOSTS.has(host) && !envRequireAuth()) {
243
219
  throw new Error(`Refusing to bind hippo serve to non-loopback host '${host}' without auth. ` +
244
220
  `Set HIPPO_REQUIRE_AUTH=1 to bind non-loopback; every request then requires ` +
245
221
  `a valid API key. Bind to 127.0.0.1 / ::1 / localhost otherwise.`);
246
222
  }
247
- // H3: refuse to start if a live hippo server already serves this hippoRoot.
223
+ }
224
+ async function assertNoLiveServer(hippoRoot) {
225
+ // Refuse to start if a live hippo server already serves this hippoRoot.
248
226
  // detectServer probes the recorded /health — a stale pidfile is unlinked and
249
227
  // ignored, but a live peer means a concurrent `hippo serve` would race for
250
228
  // the port and clobber the pidfile.
251
- const existing = await detectServer(opts.hippoRoot);
229
+ const existing = await detectServer(hippoRoot);
252
230
  if (existing) {
253
231
  throw new Error(`hippo serve: already running on port ${existing.port} (pid ${existing.pid}). ` +
254
232
  `Stop that server before starting another on the same hippoRoot.`);
255
233
  }
256
- // The server's start time. Single source of truth: it is returned by every
257
- // GET /health response and (below) written into the pidfile, so detectServer
258
- // can match the two and prove a pid-reusing impostor is not the real server.
259
- const startedAt = new Date().toISOString();
260
- // E3: per-IP rate limiter for /v1/* and /mcp*. Built here (not at module scope) so
234
+ }
235
+ function bootRateLimiter() {
236
+ // Per-IP rate limiter for /v1/* and /mcp*. Built here (not at module scope) so
261
237
  // HIPPO_V1_RPS is read at boot, matching HIPPO_PORT above and letting a test
262
238
  // set the rate before serve(). A non-positive or non-finite value disables
263
239
  // limiting (the opt-out knob).
264
240
  const v1Rps = Number(envV1Rps() ?? 20);
265
- const limiter = Number.isFinite(v1Rps) && v1Rps > 0
241
+ return Number.isFinite(v1Rps) && v1Rps > 0
266
242
  ? createRateLimiter({ ratePerSec: v1Rps, burst: v1Rps * 2, idleEvictMs: 60000, maxKeys: 10000 })
267
243
  : undefined;
268
- // Open /mcp/stream count per client key, so the cap is per server rather than per process.
269
- const streamSlots = new Map();
244
+ }
245
+ function createStoreHolder(hippoRoot) {
270
246
  // Handlers open and close their own connections; while this one is held, none of those closes is SQLite's last,
271
247
  // which checkpoints and deletes the WAL. It opens only once the store exists, so serving never creates one.
272
248
  let heldDb;
273
249
  let stopHolding = false;
274
- const holdStore = () => {
275
- if (heldDb || stopHolding || !existsSync(getHippoDbPath(opts.hippoRoot)))
250
+ const hold = () => {
251
+ if (heldDb || stopHolding || !existsSync(getHippoDbPath(hippoRoot)))
276
252
  return;
277
253
  try {
278
- heldDb = openHippoDb(opts.hippoRoot);
254
+ heldDb = openHippoDb(hippoRoot);
279
255
  }
280
256
  catch (err) {
281
257
  stopHolding = true;
282
258
  log.warn(`serve: could not hold a store connection; requests still work, only slower: ${err instanceof Error ? err.message : String(err)}`);
283
259
  }
284
260
  };
285
- const inflight = new Set();
286
- const server = createServer((req, res) => {
287
- res.once('finish', holdStore);
288
- inflight.add(res);
289
- res.once('close', () => inflight.delete(res));
290
- const requestId = resolveRequestId(req.headers['x-request-id']);
291
- requestIds.set(req, requestId);
292
- res.setHeader('X-Request-Id', requestId);
293
- withBusyWait(SERVER_DB_WAIT_MS, () => handleRequest(req, res, opts, startedAt, streamSlots, limiter)).catch((err) => {
294
- const mapped = replyFor(err);
295
- logRequestFailure(req, err, requestId, mapped.status);
296
- if (res.headersSent) {
297
- try {
298
- res.end();
299
- }
300
- catch { /* socket already gone */ }
301
- return;
302
- }
303
- if (isSqliteBusy(err))
304
- res.setHeader('Retry-After', '1');
305
- if (mapped.status === 500) {
306
- // The id lets an operator find the logged cause without the client seeing internal text.
307
- sendJson(res, 500, { error: mapped.message, requestId });
308
- return;
309
- }
310
- // RecallContractError keeps the shared {error} shape and adds `code` so clients branch without parsing prose.
311
- if (err instanceof RecallContractError) {
312
- sendJson(res, 400, { error: err.message, code: err.code });
313
- return;
314
- }
315
- sendError(res, mapped.status, mapped.message);
316
- // M3: readBody hit the 1 MB cap mid-stream, so drop the socket rather than drain unbounded bytes.
317
- if (err instanceof BodyTooLargeError)
318
- req.destroy();
319
- });
320
- });
321
- // T3b capture (v1.26.2): tests/server-concurrency.test.ts's ECONNRESET flake
322
- // traced to a chunk-boundary reuse race — a kept-alive socket idled through
323
- // a prior response chunk gets closed by the server's default 5s
324
- // keepAliveTimeout just as a client reuses it for the next request. Raising
325
- // both timeouts shrinks that idle-close/reuse window ~13x. Keep
326
- // headersTimeout ABOVE the EFFECTIVE keep-alive expiry, which is
327
- // keepAliveTimeout + keepAliveTimeoutBuffer (the buffer defaults to
328
- // 1,000ms on Node 22.19+/24.6+ — verified 1,000 on node 24.13, so the
329
- // effective expiry here is 66s; codex review caught that a 66s
330
- // headersTimeout would sit exactly ON that boundary and recreate the
331
- // race). The headers timer also runs while a kept-alive socket waits for
332
- // its next request, so a value at or below the effective expiry would
333
- // itself close idle reused sockets, and Node would not flag it (no error
334
- // or warning at listen time — verified empirically).
261
+ const release = () => {
262
+ stopHolding = true;
263
+ if (heldDb)
264
+ closeHippoDb(heldDb);
265
+ heldDb = undefined;
266
+ };
267
+ return { hold, release };
268
+ }
269
+ function replyWithFailure(req, res, err, requestId) {
270
+ const mapped = replyFor(err);
271
+ logRequestFailure(req, err, requestId, mapped.status);
272
+ if (res.headersSent) {
273
+ try {
274
+ res.end();
275
+ }
276
+ catch { /* socket already gone */ }
277
+ return;
278
+ }
279
+ if (isSqliteBusy(err))
280
+ res.setHeader('Retry-After', '1');
281
+ if (mapped.status === 500) {
282
+ // The id lets an operator find the logged cause without the client seeing internal text.
283
+ sendJson(res, 500, { error: mapped.message, requestId });
284
+ return;
285
+ }
286
+ // RecallContractError keeps the shared {error} shape and adds `code` so clients branch without parsing prose.
287
+ if (err instanceof RecallContractError) {
288
+ sendJson(res, 400, { error: err.message, code: err.code });
289
+ return;
290
+ }
291
+ sendError(res, mapped.status, mapped.message);
292
+ // readBody hit the 1 MB cap mid-stream, so drop the socket rather than drain unbounded bytes.
293
+ if (err instanceof BodyTooLargeError)
294
+ req.destroy();
295
+ }
296
+ function setKeepAliveTimeouts(server) {
297
+ // The default 5s keepAliveTimeout closes idle sockets just as clients reuse them (ECONNRESET).
298
+ // headersTimeout must stay ABOVE keepAliveTimeout + keepAliveTimeoutBuffer (1s), or it closes idle reused sockets itself.
335
299
  server.keepAliveTimeout = 65_000;
336
300
  server.headersTimeout = 70_000;
337
- await new Promise((resolve, reject) => {
301
+ }
302
+ function listenOn(server, port, host) {
303
+ return new Promise((resolve, reject) => {
338
304
  const onError = (err) => {
339
305
  server.removeListener('listening', onListening);
340
306
  reject(err);
@@ -345,17 +311,85 @@ export async function serve(opts) {
345
311
  };
346
312
  server.once('error', onError);
347
313
  server.once('listening', onListening);
348
- server.listen(requestedPort, host);
314
+ server.listen(port, host);
349
315
  });
316
+ }
317
+ function installSignalHandlers(stop) {
318
+ let shuttingDown = false;
319
+ const gracefulShutdown = async (signal) => {
320
+ if (shuttingDown)
321
+ return;
322
+ shuttingDown = true;
323
+ log.warn(`received ${signal}, shutting down`);
324
+ try {
325
+ await stop();
326
+ process.exit(0);
327
+ }
328
+ catch (err) {
329
+ log.error(`error during stop: ${err instanceof Error ? err.message : String(err)}`, errorFields(err));
330
+ process.exit(1);
331
+ }
332
+ };
333
+ process.once('SIGTERM', () => { void gracefulShutdown('SIGTERM'); });
334
+ process.once('SIGINT', () => { void gracefulShutdown('SIGINT'); });
335
+ }
336
+ /**
337
+ * Boot the HTTP daemon on host:port and write the pidfile under hippoRoot.
338
+ *
339
+ * Refuses non-loopback hosts at boot unless
340
+ * HIPPO_REQUIRE_AUTH=1 is set. The auth middleware (buildContextWithAuth /
341
+ * requireAuth) has shipped and every route checks it except GET /health
342
+ * (public by design for platform health checks) and the two connector
343
+ * webhooks in PUBLIC_ROUTES, which are HMAC-gated by their own signing
344
+ * secrets and 404 when those secrets are unset. But the loopback
345
+ * no-auth fallback inside buildContextWithAuth still admits unauthenticated
346
+ * requests from a loopback remote address (unless they carry Forwarded,
347
+ * X-Forwarded-For/-Host/-Proto, X-Real-IP, Cf-Connecting-Ip or True-Client-Ip, which mark a same-host proxy and get
348
+ * a 401 like any keyless remote request), so binding to a non-loopback host
349
+ * is only safe once that fallback is disabled with HIPPO_REQUIRE_AUTH=1,
350
+ * which forces every request (loopback or not) through Bearer-token
351
+ * validation. Without that env var set, a non-loopback bind would expose the
352
+ * DB to the network with no auth, so we fail fast instead.
353
+ *
354
+ * Use port: 0 in tests to bind to an ephemeral port and read the actual
355
+ * port back via server.address() after listen.
356
+ */
357
+ export async function serve(opts) {
358
+ const host = opts.host ?? '127.0.0.1';
359
+ const requestedPort = opts.port ?? Number(envPort() ?? 6789);
360
+ assertBindable(host);
361
+ await assertNoLiveServer(opts.hippoRoot);
362
+ // The server's start time. Single source of truth: it is returned by every
363
+ // GET /health response and (below) written into the pidfile, so detectServer
364
+ // can match the two and prove a pid-reusing impostor is not the real server.
365
+ const startedAt = new Date().toISOString();
366
+ const limiter = bootRateLimiter();
367
+ // Open /mcp/stream count per client key, so the cap is per server rather than per process.
368
+ const streamSlots = new Map();
369
+ const store = createStoreHolder(opts.hippoRoot);
370
+ const inflight = new Set();
371
+ const server = createServer((req, res) => {
372
+ res.once('finish', store.hold);
373
+ inflight.add(res);
374
+ res.once('close', () => inflight.delete(res));
375
+ const requestId = resolveRequestId(req.headers['x-request-id']);
376
+ requestIds.set(req, requestId);
377
+ res.setHeader('X-Request-Id', requestId);
378
+ withBusyWait(SERVER_DB_WAIT_MS, () => handleRequest(req, res, opts, startedAt, streamSlots, limiter)).catch((err) => {
379
+ replyWithFailure(req, res, err, requestId);
380
+ });
381
+ });
382
+ setKeepAliveTimeouts(server);
383
+ await listenOn(server, requestedPort, host);
350
384
  const address = server.address();
351
385
  if (!isAddressInfo(address)) {
352
386
  throw new Error('server.address() returned unexpected shape');
353
387
  }
354
388
  const addressInfo = address;
355
389
  const actualPort = addressInfo.port;
356
- const url = `http://${host}:${actualPort}`;
390
+ const url = `http://${host.includes(':') ? `[${host}]` : host}:${actualPort}`;
357
391
  writePidfile(opts.hippoRoot, { port: actualPort, url, startedAt });
358
- holdStore();
392
+ store.hold();
359
393
  let stopping = false;
360
394
  const stop = async () => {
361
395
  if (stopping)
@@ -363,33 +397,13 @@ export async function serve(opts) {
363
397
  stopping = true;
364
398
  // Remove the pidfile only if it still names this server. A newer server
365
399
  // may have started on this hippoRoot and rewritten the pidfile; an
366
- // unconditional unlink here would orphan it. (v0.37.0 server-hardening.)
400
+ // unconditional unlink here would orphan it.
367
401
  removePidfileIfOwned(opts.hippoRoot, { pid: process.pid, startedAt });
368
402
  await drainAndClose(server, inflight, opts.shutdownDrainMs ?? 5000);
369
- stopHolding = true;
370
- if (heldDb)
371
- closeHippoDb(heldDb);
372
- heldDb = undefined;
403
+ store.release();
373
404
  };
374
- if (opts.handleSignals) {
375
- let shuttingDown = false;
376
- const gracefulShutdown = async (signal) => {
377
- if (shuttingDown)
378
- return;
379
- shuttingDown = true;
380
- log.warn(`received ${signal}, shutting down`);
381
- try {
382
- await stop();
383
- process.exit(0);
384
- }
385
- catch (err) {
386
- log.error(`error during stop: ${err instanceof Error ? err.message : String(err)}`, errorFields(err));
387
- process.exit(1);
388
- }
389
- };
390
- process.once('SIGTERM', () => { void gracefulShutdown('SIGTERM'); });
391
- process.once('SIGINT', () => { void gracefulShutdown('SIGINT'); });
392
- }
405
+ if (opts.handleSignals)
406
+ installSignalHandlers(stop);
393
407
  return { port: actualPort, url, stop, server };
394
408
  }
395
409
  //# sourceMappingURL=server.js.map
package/dist/shared.d.ts CHANGED
@@ -5,7 +5,8 @@
5
5
  * Local .hippo/ stores are per-project.
6
6
  */
7
7
  import { MemoryEntry } from './memory.js';
8
- import type { SearchResult, ResultCost } from './search/types.js';
8
+ import { type SearchResult, type ResultCost } from './search/types.js';
9
+ import type { HybridVectorCandidates } from './search/vector.js';
9
10
  import type { DatabaseSyncLike } from './db.js';
10
11
  /**
11
12
  * Returns the path to the global Hippo store.
@@ -55,10 +56,10 @@ export interface HybridSearchOptions extends SearchOptions {
55
56
  asOf?: string;
56
57
  /** Budget cost per result, spent the same way in each store and in the merged list. */
57
58
  cost?: ResultCost;
58
- /** v0.30 / E4 — propagated to underlying hybridSearch calls.
59
+ /** Propagated to underlying hybridSearch calls.
59
60
  * Per-call > env HIPPO_SUMMARY_DEBOOST > 0.85 default. */
60
61
  summaryDeboost?: number;
61
- /** v0.30 / E4 — propagated. Default true (1.05 boost if rebuilt within 7d). */
62
+ /** Propagated. Default true (1.05 boost if rebuilt within 7d). */
62
63
  summaryFreshness?: boolean;
63
64
  /** v39 memory scope isolation: optional admission predicate applied to the
64
65
  * loaded candidate entries of BOTH stores BEFORE ranking, cross-store
@@ -66,7 +67,7 @@ export interface HybridSearchOptions extends SearchOptions {
66
67
  * its admitted duplicate in the dedupe pass, or saturate the budget.
67
68
  * Default undefined = unchanged behavior (recall paths never set it). */
68
69
  entryFilter?: (entry: MemoryEntry) => boolean;
69
- /** v1.25.0 — recall-mode scope filter, consumed by `searchBothHybrid` only.
70
+ /** Recall-mode scope filter, consumed by `searchBothHybrid` only.
70
71
  * ABSENT (undefined) is the only unfiltered mode: both stores load via
71
72
  * `loadSearchEntries` unchanged (background pipelines / eval callers).
72
73
  * PRESENT switches the internal loads to `loadRecallSearchEntries` (SQL
@@ -93,6 +94,14 @@ export interface HybridSearchOptions extends SearchOptions {
93
94
  * Async version of searchBoth that calls hybridSearch instead of search.
94
95
  */
95
96
  export declare function searchBothHybrid(query: string, localRoot: string, globalRoot: string, options?: HybridSearchOptions): Promise<SearchResult[]>;
97
+ /** Hybrid ranking of rows already loaded from each store: the local bump, one copy per text, then the shared budget. */
98
+ export declare function rankBothStores(query: string, roots: {
99
+ local: string;
100
+ global: string;
101
+ }, entries: {
102
+ local: MemoryEntry[];
103
+ global: MemoryEntry[];
104
+ }, vectorCandidates: HybridVectorCandidates, options?: HybridSearchOptions): Promise<SearchResult[]>;
96
105
  /** Tags whose rows only a hand-run share or promote may copy to the global store; derived rows inherit them. */
97
106
  export declare const NEVER_AUTO_SHARE_TAGS: ReadonlySet<string>;
98
107
  export declare function neverAutoShareTags(sources: readonly MemoryEntry[]): string[];
@@ -117,7 +126,7 @@ export declare function shareMemory(localRoot: string, id: string, options?: {
117
126
  * List all projects that have contributed memories to the global store.
118
127
  * Parses the source field for 'shared:<project>:' or 'promoted:<path>' patterns.
119
128
  *
120
- * D4 v1.12.10: `tenantId` is now optional. When provided, the global entries
129
+ * `tenantId` is optional. When provided, the global entries
121
130
  * are filtered to that tenant before aggregation — matches every other
122
131
  * read path's default-safe behaviour. When undefined, host-wide (back-compat
123
132
  * for legacy callers like CLI standalone + dashboard internal use). Operators
@@ -129,25 +138,28 @@ export declare function listPeers(globalRoot?: string, tenantId?: string): Array
129
138
  count: number;
130
139
  latest: string;
131
140
  }>;
141
+ type AutoShareStats = {
142
+ secretSkipped: number;
143
+ rejectedSkipped?: number;
144
+ neverAutoShareSkipped?: number;
145
+ };
132
146
  /**
133
147
  * Auto-share: local memories with high transfer scores, not already global, no NEVER_AUTO_SHARE_TAGS tag.
134
148
  * Returns the list of shared entries.
135
149
  *
136
- * L9: `options.tenantId` is opt-in. When provided, the LOCAL-entries read is
150
+ * `options.tenantId` is opt-in. When provided, the LOCAL-entries read is
137
151
  * scoped to that tenant. When undefined, the local read is host-wide (current
138
152
  * behaviour). The GLOBAL-entries read is always unioned — the global root IS
139
- * the cross-tenant aggregate by design. The only intentional unscoped
140
- * internal caller as of v1.12.1 is `api.sleep` (`src/api.ts:2041`), which
141
- * passes options without tenantId because `sleep` is host-wide by intent;
142
- * see `src/api.ts:2073-2077` for the cross-tenant dedup rationale.
153
+ * the cross-tenant aggregate by design. `api.sleep` passes no tenantId
154
+ * because `sleep` is host-wide by intent.
143
155
  *
144
- * v1.25.0: `options.stats` is an opt-in out-param. When provided,
156
+ * `options.stats` is an opt-in out-param. When provided,
145
157
  * `stats.secretSkipped` is incremented once per row that passed every OTHER
146
158
  * admission gate (transfer score, not-already-global) and was withheld SOLELY
147
159
  * by the secret veto — i.e. it counts shares actually prevented, not secret
148
160
  * rows merely present. Filled identically under `dryRun`.
149
161
  *
150
- * AT1: `stats.rejectedSkipped` (optional) is incremented once per candidate
162
+ * `stats.rejectedSkipped` (optional) is incremented once per candidate
151
163
  * refused by the GLOBAL store's rejection tombstone (RejectedValueError from
152
164
  * shareMemory -> writeEntry). Unlike secretSkipped, this can only be
153
165
  * detected by attempting the write — `dryRun` returns candidates before the
@@ -159,11 +171,7 @@ export declare function autoShare(localRoot: string, options?: {
159
171
  minScore?: number;
160
172
  dryRun?: boolean;
161
173
  tenantId?: string;
162
- stats?: {
163
- secretSkipped: number;
164
- rejectedSkipped?: number;
165
- neverAutoShareSkipped?: number;
166
- };
174
+ stats?: AutoShareStats;
167
175
  }): MemoryEntry[];
168
176
  /**
169
177
  * Copy all global memories into the local store.
@@ -173,4 +181,5 @@ export declare function autoShare(localRoot: string, options?: {
173
181
  export declare function syncGlobalToLocal(localRoot: string, globalRoot: string, opts?: {
174
182
  includeCrossProject?: boolean;
175
183
  }): number;
184
+ export {};
176
185
  //# sourceMappingURL=shared.d.ts.map