hippo-memory 1.62.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 (370) hide show
  1. package/README.md +9 -0
  2. package/dist/agent-memories/apply.d.ts +2 -0
  3. package/dist/agent-memories/apply.js +23 -1
  4. package/dist/agent-memories/claude-code.d.ts +2 -1
  5. package/dist/agent-memories/claude-code.js +4 -4
  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.js +37 -23
  9. package/dist/ambient-store.d.ts +4 -4
  10. package/dist/ambient-store.js +4 -3
  11. package/dist/api/assemble.d.ts +7 -10
  12. package/dist/api/assemble.js +7 -12
  13. package/dist/api/audit.d.ts +2 -2
  14. package/dist/api/audit.js +2 -2
  15. package/dist/api/auth.d.ts +6 -6
  16. package/dist/api/auth.js +5 -7
  17. package/dist/api/context-select.d.ts +4 -3
  18. package/dist/api/context-select.js +9 -11
  19. package/dist/api/context-types.d.ts +12 -17
  20. package/dist/api/context.d.ts +3 -2
  21. package/dist/api/context.js +20 -20
  22. package/dist/api/dormant.js +1 -1
  23. package/dist/api/drill-down.d.ts +4 -4
  24. package/dist/api/drill-down.js +22 -10
  25. package/dist/api/outcome.d.ts +8 -13
  26. package/dist/api/outcome.js +13 -17
  27. package/dist/api/promote.d.ts +5 -8
  28. package/dist/api/quarantine.js +3 -2
  29. package/dist/api/recall-types.d.ts +43 -55
  30. package/dist/api/recall.d.ts +3 -3
  31. package/dist/api/recall.js +7 -18
  32. package/dist/api/remember.d.ts +2 -2
  33. package/dist/api/sleep.d.ts +7 -35
  34. package/dist/api/sleep.js +3 -2
  35. package/dist/api/tokens.d.ts +2 -2
  36. package/dist/api/tokens.js +2 -2
  37. package/dist/api/types.d.ts +6 -10
  38. package/dist/api/types.js +2 -4
  39. package/dist/audit-prune.d.ts +4 -6
  40. package/dist/audit-prune.js +3 -5
  41. package/dist/audit.js +11 -33
  42. package/dist/auth.d.ts +8 -9
  43. package/dist/auth.js +5 -7
  44. package/dist/autolearn.d.ts +1 -1
  45. package/dist/autolearn.js +1 -1
  46. package/dist/availability.js +3 -5
  47. package/dist/capture/command.d.ts +5 -4
  48. package/dist/capture/command.js +8 -22
  49. package/dist/capture/compact.d.ts +1 -1
  50. package/dist/capture/compact.js +9 -13
  51. package/dist/capture/extract.js +37 -115
  52. package/dist/capture-error.d.ts +1 -1
  53. package/dist/capture-error.js +1 -1
  54. package/dist/churn-git.d.ts +1 -1
  55. package/dist/churn-git.js +1 -1
  56. package/dist/cli/audit.js +3 -4
  57. package/dist/cli/auth.js +3 -6
  58. package/dist/cli/curate.d.ts +1 -1
  59. package/dist/cli/curate.js +8 -24
  60. package/dist/cli/dag.js +5 -9
  61. package/dist/cli/decisions.js +10 -21
  62. package/dist/cli/explain.js +2 -1
  63. package/dist/cli/goals.js +1 -1
  64. package/dist/cli/init.js +1 -1
  65. package/dist/cli/playbooks.js +4 -9
  66. package/dist/cli/projects.js +3 -1
  67. package/dist/cli/recall.js +2 -1
  68. package/dist/cli/remember.js +6 -18
  69. package/dist/cli/session-hooks.js +21 -40
  70. package/dist/cli/setup.d.ts +1 -1
  71. package/dist/cli/setup.js +2 -6
  72. package/dist/cli/shared.js +3 -3
  73. package/dist/cli/slack.js +1 -1
  74. package/dist/cli/sleep.js +27 -1
  75. package/dist/cli/status.d.ts +4 -4
  76. package/dist/cli/status.js +7 -12
  77. package/dist/cli/transfer.js +8 -13
  78. package/dist/cli/usage.js +9 -6
  79. package/dist/cli.d.ts +1 -1
  80. package/dist/cli.js +4 -9
  81. package/dist/compaction-record.d.ts +2 -0
  82. package/dist/compaction-record.js +89 -68
  83. package/dist/compare.d.ts +11 -16
  84. package/dist/compare.js +11 -16
  85. package/dist/config.d.ts +19 -18
  86. package/dist/config.js +90 -67
  87. package/dist/connectors/github/backfill.d.ts +2 -2
  88. package/dist/connectors/github/backfill.js +8 -15
  89. package/dist/connectors/github/cli-impl.js +3 -8
  90. package/dist/connectors/github/deletion.d.ts +5 -12
  91. package/dist/connectors/github/deletion.js +5 -12
  92. package/dist/connectors/github/dlq.d.ts +6 -9
  93. package/dist/connectors/github/dlq.js +2 -3
  94. package/dist/connectors/github/ingest.d.ts +5 -7
  95. package/dist/connectors/github/ingest.js +8 -12
  96. package/dist/connectors/github/octokit-client.d.ts +3 -5
  97. package/dist/connectors/github/octokit-client.js +5 -6
  98. package/dist/connectors/github/signature.d.ts +9 -39
  99. package/dist/connectors/github/signature.js +9 -39
  100. package/dist/connectors/github/tenant-routing.d.ts +1 -1
  101. package/dist/connectors/github/tenant-routing.js +1 -1
  102. package/dist/connectors/github/transform.js +2 -2
  103. package/dist/connectors/github/types.d.ts +2 -10
  104. package/dist/connectors/github/types.js +1 -3
  105. package/dist/connectors/slack/deletion.d.ts +3 -8
  106. package/dist/connectors/slack/deletion.js +3 -8
  107. package/dist/connectors/slack/dlq.d.ts +1 -1
  108. package/dist/connectors/slack/ingest.d.ts +1 -1
  109. package/dist/connectors/slack/ingest.js +7 -16
  110. package/dist/connectors/slack/signature.d.ts +1 -1
  111. package/dist/connectors/slack/tenant-routing.d.ts +3 -5
  112. package/dist/connectors/slack/tenant-routing.js +3 -5
  113. package/dist/connectors/slack/transform.d.ts +5 -6
  114. package/dist/connectors/slack/transform.js +5 -6
  115. package/dist/connectors/slack/types.d.ts +2 -6
  116. package/dist/connectors/slack/types.js +1 -3
  117. package/dist/connectors/slack/web-client.js +10 -3
  118. package/dist/connectors/slack/workspaces.d.ts +3 -5
  119. package/dist/connectors/slack/workspaces.js +3 -5
  120. package/dist/consolidate/conflicts.js +3 -14
  121. package/dist/consolidate/decay.js +9 -29
  122. package/dist/consolidate/llm-passes.js +4 -5
  123. package/dist/consolidate/merge.js +8 -23
  124. package/dist/consolidate/run.d.ts +1 -8
  125. package/dist/consolidate/run.js +3 -25
  126. package/dist/consolidate/sleep.js +5 -17
  127. package/dist/consolidate/traces.js +9 -21
  128. package/dist/customer-notes.d.ts +5 -7
  129. package/dist/customer-notes.js +6 -9
  130. package/dist/dag.d.ts +10 -21
  131. package/dist/dag.js +23 -73
  132. package/dist/db/continuity.js +2 -2
  133. package/dist/db/migrations/v14.js +1 -1
  134. package/dist/db/migrations/v15.js +1 -2
  135. package/dist/db/migrations/v16.js +3 -4
  136. package/dist/db/migrations/v17.js +2 -3
  137. package/dist/db/migrations/v19.js +1 -1
  138. package/dist/db/migrations/v20.js +1 -1
  139. package/dist/db/migrations/v21.js +2 -6
  140. package/dist/db/migrations/v22.js +2 -4
  141. package/dist/db/migrations/v23.js +1 -1
  142. package/dist/db/migrations/v24.js +4 -6
  143. package/dist/db/migrations/v25.js +2 -3
  144. package/dist/db/migrations/v26.js +3 -3
  145. package/dist/db/migrations/v27.js +2 -10
  146. package/dist/db/migrations/v28.js +5 -8
  147. package/dist/db/migrations/v29.js +3 -4
  148. package/dist/db/migrations/v30.js +2 -2
  149. package/dist/db/migrations/v31.js +1 -1
  150. package/dist/db/migrations/v32.js +1 -1
  151. package/dist/db/migrations/v33.js +3 -3
  152. package/dist/db/migrations/v34.js +1 -1
  153. package/dist/db/migrations/v35.js +3 -4
  154. package/dist/db/migrations/v36.js +3 -4
  155. package/dist/db/migrations/v37.js +5 -5
  156. package/dist/db/migrations/v38.js +7 -8
  157. package/dist/db/migrations/v39.js +1 -1
  158. package/dist/db/migrations/v40.js +4 -16
  159. package/dist/db/migrations/v41.js +3 -4
  160. package/dist/db/migrations/v42.js +3 -4
  161. package/dist/db/migrations/v45.js +1 -1
  162. package/dist/db/migrations/v46.js +1 -1
  163. package/dist/db/migrations/v47.js +1 -1
  164. package/dist/db/migrations/v48.js +1 -1
  165. package/dist/decisions.d.ts +2 -2
  166. package/dist/decisions.js +6 -6
  167. package/dist/dedupe.js +86 -61
  168. package/dist/delivery-recorder.js +154 -135
  169. package/dist/doctor.js +119 -104
  170. package/dist/dormant.js +1 -4
  171. package/dist/embedding-provider.d.ts +4 -8
  172. package/dist/embedding-provider.js +4 -8
  173. package/dist/embeddings.js +55 -47
  174. package/dist/env.d.ts +1 -1
  175. package/dist/env.js +12 -12
  176. package/dist/escape.d.ts +5 -0
  177. package/dist/escape.js +10 -0
  178. package/dist/eval-stats.d.ts +1 -2
  179. package/dist/eval-stats.js +1 -2
  180. package/dist/eval-suite.js +27 -21
  181. package/dist/extract.js +4 -9
  182. package/dist/failure-log.d.ts +3 -3
  183. package/dist/failure-log.js +1 -1
  184. package/dist/forward-claim-detector.d.ts +2 -4
  185. package/dist/forward-claim-detector.js +6 -11
  186. package/dist/goals.d.ts +3 -3
  187. package/dist/goals.js +5 -6
  188. package/dist/graph/read.d.ts +2 -2
  189. package/dist/graph/read.js +5 -6
  190. package/dist/graph/types.d.ts +8 -8
  191. package/dist/graph/write.d.ts +7 -14
  192. package/dist/graph/write.js +16 -23
  193. package/dist/graph-extract.d.ts +7 -8
  194. package/dist/graph-extract.js +62 -72
  195. package/dist/graph-recall.d.ts +2 -2
  196. package/dist/graph-recall.js +55 -49
  197. package/dist/graph-stream.d.ts +5 -6
  198. package/dist/graph-stream.js +66 -57
  199. package/dist/graph-view.d.ts +2 -2
  200. package/dist/graph-view.js +7 -7
  201. package/dist/half-life-migration.d.ts +1 -2
  202. package/dist/half-life-migration.js +2 -3
  203. package/dist/hooks/codex-session.js +1 -1
  204. package/dist/hooks/codex-wrapper.d.ts +1 -1
  205. package/dist/hooks/codex-wrapper.js +3 -2
  206. package/dist/hooks/json-hooks.d.ts +2 -2
  207. package/dist/hooks/json-hooks.js +5 -4
  208. package/dist/hooks/opencode.d.ts +1 -1
  209. package/dist/hooks/opencode.js +5 -4
  210. package/dist/hooks/shared.d.ts +3 -7
  211. package/dist/hooks/shared.js +1 -8
  212. package/dist/http-util.d.ts +2 -3
  213. package/dist/http-util.js +3 -0
  214. package/dist/importers/core.d.ts +2 -9
  215. package/dist/importers/core.js +15 -30
  216. package/dist/importers/sources.js +2 -1
  217. package/dist/importers/vault.js +2 -20
  218. package/dist/incidents.d.ts +1 -1
  219. package/dist/incidents.js +1 -1
  220. package/dist/instruction-detect.d.ts +1 -1
  221. package/dist/instruction-detect.js +1 -1
  222. package/dist/invalidation.d.ts +3 -0
  223. package/dist/invalidation.js +160 -114
  224. package/dist/json.d.ts +5 -0
  225. package/dist/json.js +4 -0
  226. package/dist/judgment.js +1 -2
  227. package/dist/local-embedding.js +1 -1
  228. package/dist/mcp/admin-tools.js +7 -17
  229. package/dist/mcp/format.js +1 -1
  230. package/dist/mcp/framing.js +3 -6
  231. package/dist/mcp/protocol.d.ts +2 -5
  232. package/dist/mcp/protocol.js +1 -3
  233. package/dist/mcp/recall-tools.js +12 -15
  234. package/dist/mcp/request.js +4 -3
  235. package/dist/mcp/session-state.js +2 -3
  236. package/dist/mcp/stdio.js +2 -1
  237. package/dist/mcp/tools.js +9 -6
  238. package/dist/memory-value-weights.d.ts +5 -8
  239. package/dist/memory-value-weights.js +5 -8
  240. package/dist/memory-value.d.ts +13 -13
  241. package/dist/memory-value.js +26 -37
  242. package/dist/memory.d.ts +20 -22
  243. package/dist/memory.js +24 -48
  244. package/dist/multihop.d.ts +1 -1
  245. package/dist/multihop.js +3 -2
  246. package/dist/owner-validation.d.ts +4 -5
  247. package/dist/owner-validation.js +4 -5
  248. package/dist/physics.d.ts +4 -4
  249. package/dist/physics.js +7 -9
  250. package/dist/policies.d.ts +9 -10
  251. package/dist/policies.js +12 -14
  252. package/dist/postinstall.js +3 -6
  253. package/dist/predictions/planning-fallacy.d.ts +9 -14
  254. package/dist/predictions/planning-fallacy.js +10 -16
  255. package/dist/predictions/store.d.ts +15 -23
  256. package/dist/predictions/store.js +36 -33
  257. package/dist/processes.d.ts +2 -7
  258. package/dist/processes.js +3 -3
  259. package/dist/project-briefs.d.ts +2 -3
  260. package/dist/project-briefs.js +9 -13
  261. package/dist/project-identity.d.ts +22 -9
  262. package/dist/project-identity.js +47 -12
  263. package/dist/project-merge.d.ts +17 -2
  264. package/dist/project-merge.js +109 -27
  265. package/dist/project-remote.d.ts +12 -0
  266. package/dist/project-remote.js +138 -0
  267. package/dist/prompt-recall.js +1 -2
  268. package/dist/rate-limit.d.ts +1 -1
  269. package/dist/rate-limit.js +1 -1
  270. package/dist/raw-archive.d.ts +9 -0
  271. package/dist/raw-archive.js +70 -53
  272. package/dist/recall-history.d.ts +19 -20
  273. package/dist/recall-history.js +24 -42
  274. package/dist/recall-pipeline.js +4 -28
  275. package/dist/recall-scope.d.ts +7 -8
  276. package/dist/recall-scope.js +7 -8
  277. package/dist/recall-trace.d.ts +5 -9
  278. package/dist/recall-trace.js +6 -10
  279. package/dist/refine-llm.d.ts +1 -1
  280. package/dist/refine-llm.js +2 -2
  281. package/dist/reject-flow.d.ts +3 -4
  282. package/dist/reject-flow.js +122 -117
  283. package/dist/rejection.d.ts +5 -6
  284. package/dist/rejection.js +7 -15
  285. package/dist/rerankers/clef.d.ts +1 -1
  286. package/dist/rerankers/jev.d.ts +1 -2
  287. package/dist/rerankers/jev.js +4 -5
  288. package/dist/rerankers/llm.d.ts +1 -2
  289. package/dist/rerankers/llm.js +1 -2
  290. package/dist/rerankers/types.d.ts +1 -2
  291. package/dist/rrf.d.ts +2 -2
  292. package/dist/rrf.js +2 -2
  293. package/dist/search/bm25-search.d.ts +1 -1
  294. package/dist/search/bm25-search.js +2 -1
  295. package/dist/search/boosts.js +2 -1
  296. package/dist/search/hybrid.d.ts +1 -1
  297. package/dist/search/hybrid.js +2 -1
  298. package/dist/search/physics-search.d.ts +1 -1
  299. package/dist/search/physics-search.js +2 -1
  300. package/dist/search/types.d.ts +2 -0
  301. package/dist/search/types.js +3 -1
  302. package/dist/secret-detect.d.ts +4 -5
  303. package/dist/secret-detect.js +6 -10
  304. package/dist/server/auth.js +5 -5
  305. package/dist/server/client-ip.js +1 -1
  306. package/dist/server/cursor.js +2 -1
  307. package/dist/server/mcp-http.js +4 -4
  308. package/dist/server/request.d.ts +3 -6
  309. package/dist/server/request.js +6 -7
  310. package/dist/server/routes/admin.js +5 -4
  311. package/dist/server/routes/customer-notes.js +6 -5
  312. package/dist/server/routes/decisions.js +4 -3
  313. package/dist/server/routes/incidents.js +7 -5
  314. package/dist/server/routes/memories.js +7 -7
  315. package/dist/server/routes/policies.js +3 -2
  316. package/dist/server/routes/predictions.js +12 -15
  317. package/dist/server/routes/processes.js +3 -2
  318. package/dist/server/routes/project-briefs.js +8 -7
  319. package/dist/server/routes/recall.js +95 -93
  320. package/dist/server/routes/skills.js +6 -5
  321. package/dist/server/types.d.ts +1 -1
  322. package/dist/server/validation.d.ts +1 -2
  323. package/dist/server/validation.js +7 -14
  324. package/dist/server-detect.js +72 -58
  325. package/dist/server.d.ts +2 -2
  326. package/dist/server.js +131 -117
  327. package/dist/shared.d.ts +17 -17
  328. package/dist/shared.js +93 -97
  329. package/dist/skills.d.ts +3 -3
  330. package/dist/skills.js +8 -8
  331. package/dist/store/audit-event.d.ts +2 -2
  332. package/dist/store/audit-event.js +1 -1
  333. package/dist/store/candidates.d.ts +2 -2
  334. package/dist/store/candidates.js +4 -3
  335. package/dist/store/conflicts.js +30 -22
  336. package/dist/store/delete-and-batch.d.ts +11 -14
  337. package/dist/store/delete-and-batch.js +40 -91
  338. package/dist/store/entry-reads.d.ts +17 -25
  339. package/dist/store/entry-reads.js +59 -40
  340. package/dist/store/entry-row.d.ts +6 -24
  341. package/dist/store/entry-row.js +6 -24
  342. package/dist/store/entry-writes.d.ts +6 -7
  343. package/dist/store/entry-writes.js +13 -11
  344. package/dist/store/handoffs.d.ts +1 -1
  345. package/dist/store/handoffs.js +7 -10
  346. package/dist/store/index-and-stats.d.ts +2 -6
  347. package/dist/store/index-and-stats.js +4 -10
  348. package/dist/store/mirrors.d.ts +6 -19
  349. package/dist/store/mirrors.js +14 -39
  350. package/dist/store/open.js +9 -31
  351. package/dist/store/rows.d.ts +5 -11
  352. package/dist/store/rows.js +6 -11
  353. package/dist/store/search-rows.d.ts +17 -34
  354. package/dist/store/search-rows.js +31 -59
  355. package/dist/store/sessions.d.ts +4 -5
  356. package/dist/store/sessions.js +5 -6
  357. package/dist/store/summaries.d.ts +13 -17
  358. package/dist/store/summaries.js +26 -70
  359. package/dist/support-bundle.js +4 -8
  360. package/dist/tenant.d.ts +1 -5
  361. package/dist/token-ledger.d.ts +1 -1
  362. package/dist/token-ledger.js +3 -5
  363. package/dist/trace.js +1 -3
  364. package/dist/version.d.ts +1 -1
  365. package/dist/version.js +1 -1
  366. package/dist/working-memory.d.ts +1 -1
  367. package/extensions/openclaw-plugin/openclaw.plugin.json +1 -1
  368. package/extensions/openclaw-plugin/package.json +1 -1
  369. package/openclaw.plugin.json +1 -1
  370. package/package.json +1 -1
package/dist/rrf.d.ts CHANGED
@@ -4,13 +4,13 @@
4
4
  * Fuses N ranked candidate lists into a single ordering by summing
5
5
  * weighted 1/(k + rank) contributions per candidate. The constant K is
6
6
  * the canonical 60 from the original paper and the value already in use
7
- * across hippo's `hybridSearch` since v1.0. Do NOT tune K without an
7
+ * across hippo's `hybridSearch`. Do NOT tune K without an
8
8
  * explicit cross-corpus eval — it is calibrated against IR benchmarks
9
9
  * and works robustly across BM25/dense/cross-encoder rank-list shapes.
10
10
  *
11
11
  * Generic over the candidate id type so this helper can be shared by
12
12
  * `src/search.ts::hybridSearch` (T = number, idx into MemoryEntry[]) and
13
- * the LongMemEval F9 hybrid retrieve benchmark (T = string, session_id).
13
+ * the LongMemEval hybrid retrieve benchmark (T = string, session_id).
14
14
  *
15
15
  * Behaviour MUST stay byte-identical to the inline implementation that
16
16
  * lived in `src/search.ts:354-374` before extraction (commit ab6c5eb).
package/dist/rrf.js CHANGED
@@ -4,13 +4,13 @@
4
4
  * Fuses N ranked candidate lists into a single ordering by summing
5
5
  * weighted 1/(k + rank) contributions per candidate. The constant K is
6
6
  * the canonical 60 from the original paper and the value already in use
7
- * across hippo's `hybridSearch` since v1.0. Do NOT tune K without an
7
+ * across hippo's `hybridSearch`. Do NOT tune K without an
8
8
  * explicit cross-corpus eval — it is calibrated against IR benchmarks
9
9
  * and works robustly across BM25/dense/cross-encoder rank-list shapes.
10
10
  *
11
11
  * Generic over the candidate id type so this helper can be shared by
12
12
  * `src/search.ts::hybridSearch` (T = number, idx into MemoryEntry[]) and
13
- * the LongMemEval F9 hybrid retrieve benchmark (T = string, session_id).
13
+ * the LongMemEval hybrid retrieve benchmark (T = string, session_id).
14
14
  *
15
15
  * Behaviour MUST stay byte-identical to the inline implementation that
16
16
  * lived in `src/search.ts:354-374` before extraction (commit ab6c5eb).
@@ -1,5 +1,5 @@
1
1
  import type { MemoryEntry } from '../memory.js';
2
- import type { ResultCost, SearchResult } from './types.js';
2
+ import { type ResultCost, type SearchResult } from './types.js';
3
3
  export interface SearchOptions {
4
4
  budget?: number;
5
5
  now?: Date;
@@ -9,6 +9,7 @@ import { currentEntries } from './as-of.js';
9
9
  import { applyRankBoosts, strengthRecencyMultipliers, NO_SUMMARY_SCORING } from './boosts.js';
10
10
  import { temporalContext } from './temporal.js';
11
11
  import { dedupeExtracted, fitBudget, withDagChildren } from './finalize.js';
12
+ import { DEFAULT_RECALL_BUDGET } from './types.js';
12
13
  /** Synchronous BM25 search: relevance * strength * recency and the rank boosts, without embeddings or the outcome nudge. */
13
14
  export function search(query, entries, options = {}) {
14
15
  const now = options.now ?? evalNow(); // honors HIPPO_FAKE_NOW (eval-only; see ablation.ts)
@@ -38,6 +39,6 @@ export function search(query, entries, options = {}) {
38
39
  scored.push({ entry: pool[i], score, bm25, cosine: 0, tokens: estimateTokens(pool[i].content) });
39
40
  }
40
41
  scored.sort(compareScoredResults);
41
- return fitBudget(withDagChildren(dedupeExtracted(scored), pool), options.budget ?? 4000, options.minResults ?? 1, options.cost);
42
+ return fitBudget(withDagChildren(dedupeExtracted(scored), pool), options.budget ?? DEFAULT_RECALL_BUDGET, options.minResults ?? 1, options.cost);
42
43
  }
43
44
  //# sourceMappingURL=bm25-search.js.map
@@ -5,6 +5,7 @@ import { pathBoostMultiplier } from '../path-context.js';
5
5
  import { scopeMatch } from '../scope.js';
6
6
  import { temporalBoost } from './temporal.js';
7
7
  export const CHURN_STALE_RANK_MULTIPLIER = 0.5; // SHORTCUT: untuned; measure on real recall before any default.
8
+ const DECISION_TAG_BOOST = 1.2;
8
9
  const DEFAULT_SUMMARY_DEBOOST = 0.85;
9
10
  const DEFAULT_FRESHNESS_BOOST = 1.05;
10
11
  const FRESHNESS_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
@@ -69,7 +70,7 @@ export function strengthRecencyMultipliers(entry, now) {
69
70
  }
70
71
  /** Applies every rank multiplier one at a time, in a fixed order, so float rounding matches across paths. */
71
72
  export function applyRankBoosts(score, entry, ctx) {
72
- const decisionBoost = entry.tags.includes('decision') ? 1.2 : 1.0;
73
+ const decisionBoost = entry.tags.includes('decision') ? DECISION_TAG_BOOST : 1.0;
73
74
  const churnStaleMultiplier = churnStaleFactor(entry);
74
75
  const pathBoost = pathBoostMultiplier(entry.tags, ctx.pathTags);
75
76
  const outcomeBoost = ctx.outcome ? outcomeMultiplier(entry) : 1.0;
@@ -3,7 +3,7 @@ import type { RerankerFn, RerankerOptions } from '../rerankers/types.js';
3
3
  import { type BM25Corpus } from './bm25.js';
4
4
  import { type HybridVectorCandidates } from './vector.js';
5
5
  import { type GraphStreamOptions } from './fusion.js';
6
- import type { ResultCost, SearchResult } from './types.js';
6
+ import { type ResultCost, type SearchResult } from './types.js';
7
7
  export interface HybridSearchOptions {
8
8
  budget?: number;
9
9
  now?: Date;
@@ -12,6 +12,7 @@ import { fuseRanks } from './fusion.js';
12
12
  import { scoreHybridPool } from './hybrid-score.js';
13
13
  import { applyMmrWindow, applyReranker } from './rerank.js';
14
14
  import { dedupeExtracted, fitBudget, withDagChildren } from './finalize.js';
15
+ import { DEFAULT_RECALL_BUDGET } from './types.js';
15
16
  /** BM25 blended with cosine similarity when stored vectors and a provider are available, BM25 * strength * recency otherwise. */
16
17
  export async function hybridSearch(query, entries, options = {}) {
17
18
  const now = options.now ?? evalNow(); // honors HIPPO_FAKE_NOW (eval-only; see ablation.ts)
@@ -42,7 +43,7 @@ export async function hybridSearch(query, entries, options = {}) {
42
43
  });
43
44
  scored.sort(compareScoredResults);
44
45
  const ordered = await orderHybrid(query, withDagChildren(dedupeExtracted(scored), arm.entries), arm, options);
45
- return fitBudget(ordered, options.budget ?? 4000, options.minResults ?? 1, options.cost);
46
+ return fitBudget(ordered, options.budget ?? DEFAULT_RECALL_BUDGET, options.minResults ?? 1, options.cost);
46
47
  }
47
48
  function hybridBoostContext(query, pool, now, options) {
48
49
  return {
@@ -1,7 +1,7 @@
1
1
  import { type MemoryEntry } from '../memory.js';
2
2
  import { type PhysicsConfig } from '../physics-config.js';
3
3
  import { type HybridVectorCandidates } from './vector.js';
4
- import type { ResultCost, SearchResult } from './types.js';
4
+ import { type ResultCost, type SearchResult } from './types.js';
5
5
  export interface PhysicsSearchOptions {
6
6
  budget?: number;
7
7
  now?: Date;
@@ -15,6 +15,7 @@ import { fitBudget } from './finalize.js';
15
15
  import { hybridSearch } from './hybrid.js';
16
16
  import { currentEntries } from './as-of.js';
17
17
  import { vectorCandidatesOutside } from './vector.js';
18
+ import { DEFAULT_RECALL_BUDGET } from './types.js';
18
19
  /** Scores memories by gravity, momentum and cluster amplification; rows without physics state rank through hybridSearch. */
19
20
  export async function physicsSearch(query, entries, options = {}) {
20
21
  const now = options.now ?? evalNow(); // honors HIPPO_FAKE_NOW (eval-only; see ablation.ts)
@@ -41,7 +42,7 @@ export async function physicsSearch(query, entries, options = {}) {
41
42
  : [];
42
43
  const merged = mergeScorePools(physicsResults, classicResults);
43
44
  merged.sort(compareScoredResults);
44
- return fitBudget(merged, options.budget ?? 4000, options.minResults ?? 1, options.cost);
45
+ return fitBudget(merged, options.budget ?? DEFAULT_RECALL_BUDGET, options.minResults ?? 1, options.cost);
45
46
  }
46
47
  /** The caller's vector, else the provider's; null sends the caller to hybridSearch. */
47
48
  async function physicsQueryVector(query, root, given) {
@@ -1,4 +1,6 @@
1
1
  import type { MemoryEntry } from '../memory.js';
2
+ export declare const DEFAULT_RECALL_BUDGET = 4000;
3
+ export declare const DEFAULT_LOCAL_BUMP = 1.2;
2
4
  /** One score mutation applied after candidate generation; allocated only under `recall --why` or `RecallOpts.explain`. */
3
5
  export interface RerankStep {
4
6
  /** One of: interference, value, utility, reranker, goal-boost, retrieval-count-downweight. */
@@ -1,2 +1,4 @@
1
- export {};
1
+ export const DEFAULT_RECALL_BUDGET = 4000;
2
+ // Local memories outrank global ones by this factor; getContext applies it as a 1/x global discount.
3
+ export const DEFAULT_LOCAL_BUMP = 1.2;
2
4
  //# sourceMappingURL=types.js.map
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Secret detection for memory content (v39 memory scope isolation, S4;
3
- * docs/plans/2026-07-01-memory-scope-isolation.md).
2
+ * Secret detection for memory content (memory scope isolation).
4
3
  *
5
4
  * Conservative, provider-bounded patterns only - a bare `sk-` in prose must
6
5
  * NOT flag. Detection gates two surfaces:
@@ -11,7 +10,7 @@
11
10
  * project origin. Explicit recall is unaffected - recalling a secret is
12
11
  * a deliberate act.
13
12
  *
14
- * This is deliberately a thin slice of the A4 lifecycle-compliance item
13
+ * This is deliberately a thin slice of lifecycle compliance
15
14
  * (no PII detection). Write-time scrubbing covers only text no person
16
15
  * typed into hippo; see `vetSecrets`.
17
16
  *
@@ -37,8 +36,8 @@ export declare function detectSecret(entry: {
37
36
  * Replace secret-shaped substrings in free text with a redaction marker.
38
37
  * Reuses the same `SECRET_PATTERNS` / co-occurrence guard as `detectSecret`
39
38
  * (which only flags whole-entry content) so callers that must persist raw
40
- * text that never passes through the normal capture content gate — e.g. the
41
- * CS1 pre-compact snapshot fields — can scrub it in place instead.
39
+ * text that never passes through the normal capture content gate (e.g. the
40
+ * pre-compact snapshot fields) can scrub it in place instead.
42
41
  */
43
42
  export declare function redactSecrets(text: string): string;
44
43
  /** Memories never hold a raw email address (AGENTS.md); phone numbers are left alone, as their patterns misfire on ids. */
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Secret detection for memory content (v39 memory scope isolation, S4;
3
- * docs/plans/2026-07-01-memory-scope-isolation.md).
2
+ * Secret detection for memory content (memory scope isolation).
4
3
  *
5
4
  * Conservative, provider-bounded patterns only - a bare `sk-` in prose must
6
5
  * NOT flag. Detection gates two surfaces:
@@ -11,7 +10,7 @@
11
10
  * project origin. Explicit recall is unaffected - recalling a secret is
12
11
  * a deliberate act.
13
12
  *
14
- * This is deliberately a thin slice of the A4 lifecycle-compliance item
13
+ * This is deliberately a thin slice of lifecycle compliance
15
14
  * (no PII detection). Write-time scrubbing covers only text no person
16
15
  * typed into hippo; see `vetSecrets`.
17
16
  *
@@ -48,11 +47,8 @@ const SECRET_PATTERNS = [
48
47
  // Generic assignment: api_key=..., password: ..., token = "..." where the
49
48
  // value actually LOOKS like a credential: 12+ chars drawn only from
50
49
  // token-safe characters AND containing at least one digit. Without both
51
- // constraints this pattern flags ordinary code snippets and prose -
52
- // `token = estimateTokens(entry.content)` and "the secret: incremental-
53
- // rollout worked well" were verified false positives that would silently
54
- // hide real code-lesson memories from ambient context (post-merge
55
- // adversarial review, 2026-07-02).
50
+ // constraints this pattern flags ordinary code snippets and prose
51
+ // (`token = estimateTokens(...)`), silently hiding code-lesson memories from ambient context.
56
52
  { name: 'secret-assignment', re: /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*['"]?(?=[A-Za-z0-9_\-+/]*\d)[A-Za-z0-9_\-+/=]{12,}/i },
57
53
  ];
58
54
  const KEYISH_CONTEXT_RE = /key|token|secret|credential|bearer|auth|password/i;
@@ -86,8 +82,8 @@ export function detectSecret(entry) {
86
82
  * Replace secret-shaped substrings in free text with a redaction marker.
87
83
  * Reuses the same `SECRET_PATTERNS` / co-occurrence guard as `detectSecret`
88
84
  * (which only flags whole-entry content) so callers that must persist raw
89
- * text that never passes through the normal capture content gate — e.g. the
90
- * CS1 pre-compact snapshot fields — can scrub it in place instead.
85
+ * text that never passes through the normal capture content gate (e.g. the
86
+ * pre-compact snapshot fields) can scrub it in place instead.
91
87
  */
92
88
  export function redactSecrets(text) {
93
89
  return redactText(text, false);
@@ -3,9 +3,9 @@ import { envRequireAuth } from '../env.js';
3
3
  import { resolveTenantId } from '../tenant.js';
4
4
  import { log } from '../log.js';
5
5
  import { API_KEY_PREFIX, verifyApiKeyCached } from '../auth.js';
6
- import { HttpError, isCrossSite, isHeaderString, LOOPBACK_HOST_HEADER } from '../http-util.js';
6
+ import { HttpError, isCrossSite, isHeaderString, LOOPBACK_HOST_HEADER, MAX_ID_LEN } from '../http-util.js';
7
7
  import { requestIds } from './request.js';
8
- import { isJsonString } from './validation.js';
8
+ import { isJsonString } from '../json.js';
9
9
  /**
10
10
  * Recognise loopback remote addresses. Node reports IPv6-mapped IPv4 as
11
11
  * '::ffff:127.0.0.1' on dual-stack sockets, so we accept that alongside
@@ -86,9 +86,9 @@ function sanitiseResolved(r) {
86
86
  return null;
87
87
  // Core reserves `__`-prefixed tenants (`__host__`, `__unroutable__`).
88
88
  const tenant = tenantId.trim();
89
- if (tenant.startsWith('__') || tenant.length > 256 || hasControlChar(tenant))
89
+ if (tenant.startsWith('__') || tenant.length > MAX_ID_LEN || hasControlChar(tenant))
90
90
  return null;
91
- if (!isJsonString(subject) || subject.length < 1 || subject.length > 256)
91
+ if (!isJsonString(subject) || subject.length < 1 || subject.length > MAX_ID_LEN)
92
92
  return null;
93
93
  // Padding would let "system " pass the reserved-name check yet read as `system` in an audit log.
94
94
  if (hasControlChar(subject) || subject !== subject.trim())
@@ -174,7 +174,7 @@ export async function buildContextWithAuth(req, opts) {
174
174
  throw new HttpError(401, 'auth required');
175
175
  }
176
176
  assertLocalCaller(req);
177
- // v1.12.0: loopback fallback is process-local, treat as admin.
177
+ // Loopback fallback is process-local, treat as admin.
178
178
  return {
179
179
  hippoRoot: opts.hippoRoot,
180
180
  tenantId: resolveTenantId({}),
@@ -73,7 +73,7 @@ function isTrustedProxy(list, ip) {
73
73
  return family !== 0 && list.check(bare, family === 6 ? 'ipv6' : 'ipv4');
74
74
  }
75
75
  export function enforceRateLimit(req, path, limiter) {
76
- // E3: per-IP rate limit on /v1/* and /mcp* to bound api-key-id enumeration. /health
76
+ // Per-IP rate limit on /v1/* and /mcp* to bound api-key-id enumeration. /health
77
77
  // (a liveness probe) and other paths are never throttled. A 429 thrown
78
78
  // here lands in the createServer catch like any other HttpError.
79
79
  //
@@ -1,5 +1,6 @@
1
1
  import { HttpError } from '../http-util.js';
2
- import { isJsonNumber, isJsonString } from './validation.js';
2
+ import { isJsonNumber } from './validation.js';
3
+ import { isJsonString } from '../json.js';
3
4
  /** Bare-array routes cannot grow a body field without breaking clients, so their next cursor rides this header. */
4
5
  export const NEXT_CURSOR_HEADER = 'X-Next-Cursor';
5
6
  const MAX_CURSOR_CHARS = 1024;
@@ -6,7 +6,7 @@ import { HttpError, isJsonObjectRecord, readBody, sendJson } from '../http-util.
6
6
  import { buildContextWithAuth, heartbeatVerdict, readAuthHeader, requireAuth } from './auth.js';
7
7
  import { clientIpForRateLimit } from './client-ip.js';
8
8
  import { requestIds } from './request.js';
9
- import { isJsonString } from './validation.js';
9
+ import { isJsonString } from '../json.js';
10
10
  /**
11
11
  * Build a per-client key for MCP state isolation under HTTP-MCP. Used by
12
12
  * mcp/server.ts to scope `lastRecalledIds` to the calling client so two
@@ -26,7 +26,7 @@ function buildMcpClientKey(req) {
26
26
  const addr = req.socket.remoteAddress ?? 'unknown';
27
27
  return `http:${tokenHash}:${addr}`;
28
28
  }
29
- // ── MCP-over-HTTP/SSE transport (Task 11) ──
29
+ // ── MCP-over-HTTP/SSE transport ──
30
30
  //
31
31
  // Two routes implement an MCP HTTP transport alongside the stdio one. Both
32
32
  // dispatch to the same `handleMcpRequest` as the stdio loop in src/mcp/server.ts.
@@ -71,7 +71,7 @@ export async function handleMcpPost(req, res, opts) {
71
71
  mcpRes = await handleMcpRequest(rpcReq, {
72
72
  hippoRoot: ctx.hippoRoot,
73
73
  tenantId: ctx.tenantId,
74
- // v1.12.0: McpContext.actor stays string; extract subject at the boundary.
74
+ // McpContext.actor stays string; extract subject at the boundary.
75
75
  actor: ctx.actor.subject,
76
76
  // The caller's real role: MCP tools must not run a member key as admin.
77
77
  role: ctx.actor.role,
@@ -131,7 +131,7 @@ export async function handleMcpStream(req, res, opts, streamSlots) {
131
131
  // Initial ping so smoke tests can confirm the stream is live without
132
132
  // waiting for the first keepalive interval.
133
133
  res.write(': ping\n\n');
134
- // v0.39 SSE hardening:
134
+ // SSE hardening:
135
135
  // - Heartbeat re-validates the bearer (default 60s). If the key was
136
136
  // revoked or rotated, close the stream with reason='auth_revoked'.
137
137
  // - MCP_SSE_MAX_AGE_SEC (default 3600) caps stream lifetime; close
@@ -1,7 +1,7 @@
1
1
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
2
  /** The caller's `X-Request-Id` when it is a short plain token, else a fresh UUID. */
3
3
  export declare function resolveRequestId(header: string | string[] | undefined): string;
4
- /** One line per failed request; 4xx is the caller's mistake, so it stays below the default level and skips the stack. */
4
+ /** One line per failed request; 4xx is the caller's mistake and a busy 503 is back-pressure the client retries, so neither logs as an error. */
5
5
  export declare function logRequestFailure<E>(req: IncomingMessage, err: E, requestId: string, status: number): void;
6
6
  /** The status and client message for a failed request; a held write lock is a retryable 503, never a 500. */
7
7
  export declare function replyFor<E>(err: E): {
@@ -22,11 +22,8 @@ export declare const requestIds: WeakMap<IncomingMessage, string>;
22
22
  * them — otherwise `%2F` becomes `/`, path-split runs, and the route either
23
23
  * silently 404s or matches the wrong template.
24
24
  *
25
- * codex round 3 P2: only scan the PATHNAME portion of the raw URL, not the
26
- * query string. Pre-fix, `?q=https%3A%2F%2Fexample.com` would 400 because
27
- * the regex matched `%2F` anywhere in `req.url`. Recall queries containing
28
- * URLs would have been rejected as bypass attempts. Splitting on the first
29
- * `?` confines the check to the path.
25
+ * Only the PATHNAME is scanned (split on the first `?`), so recall queries
26
+ * containing URLs like `?q=https%3A%2F%2Fexample.com` are not rejected.
30
27
  */
31
28
  export declare function rejectEncodedSlash(rawUrl: string): void;
32
29
  export {};
@@ -9,11 +9,13 @@ export function resolveRequestId(header) {
9
9
  const value = Array.isArray(header) ? undefined : header;
10
10
  return value && REQUEST_ID_RE.test(value) ? value : randomUUID();
11
11
  }
12
- /** One line per failed request; 4xx is the caller's mistake, so it stays below the default level and skips the stack. */
12
+ /** One line per failed request; 4xx is the caller's mistake and a busy 503 is back-pressure the client retries, so neither logs as an error. */
13
13
  export function logRequestFailure(req, err, requestId, status) {
14
14
  const message = err instanceof Error ? err.message : String(err);
15
15
  const line = `${req.method ?? 'GET'} ${(req.url ?? '/').split('?')[0]} failed: ${message}`;
16
- if (status >= 500)
16
+ if (isSqliteBusy(err))
17
+ log.warn(line, { requestId, status });
18
+ else if (status >= 500)
17
19
  log.error(line, { requestId, status, ...errorFields(err) });
18
20
  else
19
21
  log.info(line, { requestId, status });
@@ -87,11 +89,8 @@ export const requestIds = new WeakMap();
87
89
  * them — otherwise `%2F` becomes `/`, path-split runs, and the route either
88
90
  * silently 404s or matches the wrong template.
89
91
  *
90
- * codex round 3 P2: only scan the PATHNAME portion of the raw URL, not the
91
- * query string. Pre-fix, `?q=https%3A%2F%2Fexample.com` would 400 because
92
- * the regex matched `%2F` anywhere in `req.url`. Recall queries containing
93
- * URLs would have been rejected as bypass attempts. Splitting on the first
94
- * `?` confines the check to the path.
92
+ * Only the PATHNAME is scanned (split on the first `?`), so recall queries
93
+ * containing URLs like `?q=https%3A%2F%2Fexample.com` are not rejected.
95
94
  */
96
95
  export function rejectEncodedSlash(rawUrl) {
97
96
  const queryIdx = rawUrl.indexOf('?');
@@ -4,7 +4,8 @@ import { auditList, authCreate, authListRows, authRevoke, quarantineApprove, qua
4
4
  import { HttpError, sendJson } from '../../http-util.js';
5
5
  import { assertCrossTenantAdmin, buildContextWithAuth } from '../auth.js';
6
6
  import { pageOf, parseCursor, setNextCursorHeader } from '../cursor.js';
7
- import { isJsonString, isSetMember, parseJsonBody, parseListLimit, validateIdSegment } from '../validation.js';
7
+ import { isSetMember, parseJsonBody, parseListLimit, validateIdSegment } from '../validation.js';
8
+ import { isJsonString } from '../../json.js';
8
9
  const VALID_AUDIT_OPS = new Set(AUDIT_OPS);
9
10
  // Cap on GET /v1/audit?limit=. Matches docs/api.md (when written) and is large
10
11
  // enough to dump a small deployment's full audit log without paginating, but
@@ -13,7 +14,7 @@ const MAX_AUDIT_LIMIT = 10000;
13
14
  // GET /v1/auth/keys had no limit, so its default page is the cap: a deployment below 1000 keys sees no change.
14
15
  const MAX_AUTH_KEYS_PAGE = 1000;
15
16
  // POST /v1/auth/keys — mint a new API key. Plaintext lands in the response
16
- // body (Task 8): the HTTP layer hands it to the client; the user-facing
17
+ // body: the HTTP layer hands it to the client; the user-facing
17
18
  // "store this somewhere safe" warning belongs in the CLI client, not here.
18
19
  export async function handleCreateAuthKey({ req, res, opts }) {
19
20
  const ctx = await buildContextWithAuth(req, opts);
@@ -22,7 +23,7 @@ export async function handleCreateAuthKey({ req, res, opts }) {
22
23
  if (labelRaw !== undefined && !isJsonString(labelRaw)) {
23
24
  throw new HttpError(400, 'label must be a string');
24
25
  }
25
- // v1.12.3: optional body.role mirrors the --role CLI flag. Validated
26
+ // Optional body.role mirrors the --role CLI flag. Validated
26
27
  // strictly — anything other than 'admin'|'member' is a 400 (no silent
27
28
  // fallback to admin). authCreate refuses a member caller with a 403.
28
29
  const roleRaw = body['role'];
@@ -76,7 +77,7 @@ export async function handleRevokeAuthKey({ req, res, opts }, keyMatch) {
76
77
  sendJson(res, 200, result);
77
78
  return;
78
79
  }
79
- // GET /v1/quarantine?status=&limit=&cursor=: CD5 review queue. quarantineList carries no role gate itself, so it's checked here.
80
+ // GET /v1/quarantine?status=&limit=&cursor=: quarantine review queue. quarantineList carries no role gate itself, so it's checked here.
80
81
  export async function handleListQuarantine({ req, res, opts, query }) {
81
82
  const ctx = await buildContextWithAuth(req, opts);
82
83
  if (ctx.actor.role !== 'admin') {
@@ -1,10 +1,11 @@
1
1
  // /v1/customer-notes routes.
2
- import { closeCustomerNote, loadCustomerNoteById, loadCustomerNotes, saveCustomerNote, VALID_NOTE_STATES } from '../../customer-notes.js';
2
+ import { closeCustomerNote, loadCustomerNoteById, loadCustomerNotes, MAX_CUSTOMER_LEN, saveCustomerNote, VALID_NOTE_STATES } from '../../customer-notes.js';
3
3
  import { HttpError, sendJson } from '../../http-util.js';
4
4
  import { buildContextWithAuth } from '../auth.js';
5
5
  import { byCreatedAt, pageOf, parseCursor } from '../cursor.js';
6
- import { isJsonString, isSetMember, parseJsonBody, parseListLimit } from '../validation.js';
7
- // ── E2 customer_note routes ──
6
+ import { isSetMember, parseJsonBody, parseListLimit } from '../validation.js';
7
+ import { isJsonString } from '../../json.js';
8
+ // ── customer_note routes ──
8
9
  //
9
10
  // 5 routes (no assembler/refresh): POST /v1/customer-notes (new; body customer +
10
11
  // note), GET /v1/customer-notes (list; status + customer filter; shared
@@ -19,8 +20,8 @@ export async function handleCreateCustomerNote({ req, res, opts }) {
19
20
  if (!isJsonString(customer) || customer.trim().length === 0) {
20
21
  throw new HttpError(400, 'customer is required (non-empty string)');
21
22
  }
22
- if (customer.length > 256) {
23
- throw new HttpError(400, 'customer exceeds 256-character cap');
23
+ if (customer.length > MAX_CUSTOMER_LEN) {
24
+ throw new HttpError(400, `customer exceeds ${MAX_CUSTOMER_LEN}-character cap`);
24
25
  }
25
26
  const note = body['note'];
26
27
  if (!isJsonString(note) || note.trim().length === 0) {
@@ -4,15 +4,16 @@ import { HttpError, sendJson } from '../../http-util.js';
4
4
  import { NotFoundError } from '../../api-errors.js';
5
5
  import { buildContextWithAuth } from '../auth.js';
6
6
  import { byCreatedAt, pageOf, parseCursor } from '../cursor.js';
7
- import { isJsonNumber, isJsonString, isSetMember, parseJsonBody, parseListLimit } from '../validation.js';
8
- // ── decisions (E2 first-class object) ──
7
+ import { isJsonNumber, isSetMember, parseJsonBody, parseListLimit } from '../validation.js';
8
+ import { isJsonString } from '../../json.js';
9
+ // ── decisions (first-class object) ──
9
10
  //
10
11
  // 5 routes: POST /v1/decisions (create, optional supersedesDecisionId),
11
12
  // GET /v1/decisions (list, status filter), GET /v1/decisions/:id (show),
12
13
  // POST /v1/decisions/:id/supersede (create a successor + supersede :id),
13
14
  // POST /v1/decisions/:id/close (retire). Bearer-authed + tenant-scoped via
14
15
  // buildContextWithAuth. status validated against VALID_DECISION_STATES.
15
- // DoS caps: text 4096, context 4096 (v1.11.4 pattern). The HTTP surface is
16
+ // DoS caps: text 4096, context 4096. The HTTP surface is
16
17
  // new (no legacy --supersedes <memory-id> constraint), so it supersedes by
17
18
  // table id and never weakens a memory mirror.
18
19
  export async function handleCreateDecision({ req, res, opts }) {
@@ -4,8 +4,10 @@ import { HttpError, sendJson } from '../../http-util.js';
4
4
  import { NotFoundError } from '../../api-errors.js';
5
5
  import { buildContextWithAuth } from '../auth.js';
6
6
  import { byCreatedAt, pageOf, parseCursor } from '../cursor.js';
7
- import { isJsonString, isSetMember, parseJsonBody, parseListLimit } from '../validation.js';
8
- // ── incidents (E2 first-class object) ──
7
+ import { isSetMember, parseJsonBody, parseListLimit } from '../validation.js';
8
+ import { isJsonString } from '../../json.js';
9
+ const MAX_LINKED_MEMORY_IDS = 256;
10
+ // ── incidents (first-class object) ──
9
11
  //
10
12
  // 5 routes: POST /v1/incidents (open; body text + context + linkedMemoryIds[]),
11
13
  // GET /v1/incidents (list, status filter), GET /v1/incidents/:id (show),
@@ -13,7 +15,7 @@ import { isJsonString, isSetMember, parseJsonBody, parseListLimit } from '../val
13
15
  // POST /v1/incidents/:id/close (open|resolved -> closed). Bearer-authed +
14
16
  // tenant-scoped via buildContextWithAuth. status validated against
15
17
  // VALID_INCIDENT_STATES. DoS caps: text 4096, context 4096, resolutionText
16
- // 4096 (v1.11.4 pattern). Mirrors /v1/decisions; lifecycle is
18
+ // 4096. Mirrors /v1/decisions; lifecycle is
17
19
  // open->resolved->closed (no supersede), so linkedMemoryIds replaces
18
20
  // supersedesDecisionId on create.
19
21
  export async function handleCreateIncident({ req, res, opts }) {
@@ -43,8 +45,8 @@ export async function handleCreateIncident({ req, res, opts }) {
43
45
  if (!Array.isArray(linkedRaw)) {
44
46
  throw new HttpError(400, 'linkedMemoryIds must be an array of memory ids');
45
47
  }
46
- if (linkedRaw.length > 256) {
47
- throw new HttpError(400, 'linkedMemoryIds exceeds 256-item cap');
48
+ if (linkedRaw.length > MAX_LINKED_MEMORY_IDS) {
49
+ throw new HttpError(400, `linkedMemoryIds exceeds ${MAX_LINKED_MEMORY_IDS}-item cap`);
48
50
  }
49
51
  const isValidMemoryId = (item) => isJsonString(item) && item.length > 0 && item.length <= 4096;
50
52
  if (!linkedRaw.every(isValidMemoryId)) {
@@ -4,7 +4,8 @@ import { buildGraphModel } from '../../graph-view.js';
4
4
  import { MAX_ENTITY_NAME_LEN } from '../../graph/types.js';
5
5
  import { HttpError, sendJson } from '../../http-util.js';
6
6
  import { assertCrossTenantAdmin, buildContextWithAuth, isLoopback } from '../auth.js';
7
- import { getString, getStringArray, isJsonBoolean, isJsonString, isSetMember, parseJsonBody, parseListLimit, validateIdSegment } from '../validation.js';
7
+ import { getString, getStringArray, isJsonBoolean, isSetMember, parseJsonBody, parseListLimit, validateIdSegment } from '../validation.js';
8
+ import { isJsonString } from '../../json.js';
8
9
  const VALID_KINDS = new Set([
9
10
  'raw',
10
11
  'distilled',
@@ -38,7 +39,7 @@ export async function handleCreateMemory({ req, res, opts }) {
38
39
  export async function handleGetGraph({ req, res, opts, query }) {
39
40
  const entityRaw = query.get('entity');
40
41
  // Cap at the graph entity-name cap (512), not the id-shaped 256, so a valid
41
- // long decision/policy name remains focusable over HTTP (codex P2).
42
+ // long decision/policy name remains focusable over HTTP.
42
43
  if (entityRaw !== null && entityRaw.length > MAX_ENTITY_NAME_LEN) {
43
44
  throw new HttpError(400, `entity exceeds the ${MAX_ENTITY_NAME_LEN}-character cap`);
44
45
  }
@@ -113,7 +114,7 @@ export async function handleApplyOutcome({ req, res, opts }) {
113
114
  if (!idsRaw.every(isNonEmptyId)) {
114
115
  throw new HttpError(400, 'ids must be an array of non-empty strings');
115
116
  }
116
- // v1.11.5: DoS cap on ids.length. Each id triggers ~3 DB ops (readEntry +
117
+ // DoS cap on ids.length. Each id triggers ~3 DB ops (readEntry +
117
118
  // writeEntry + appendAuditEvent). N=1000 keeps per-request work bounded
118
119
  // to sub-second wall time on SQLite hot path. Cap BEFORE buildContextWithAuth
119
120
  // so attack traffic doesn't pay the api-key lookup cost.
@@ -141,8 +142,7 @@ export async function handleApplyOutcome({ req, res, opts }) {
141
142
  // Tenant scope (Episode A follow-up tracked in TODOS.md): api.sleep operates
142
143
  // on the WHOLE hippoRoot (cross-tenant by design, matching CLI cmdSleep).
143
144
  // The loopback-only guard is the trust boundary today. Future non-loopback
144
- // serving must also zero the cross-tenant counters for other tenants
145
- // (D1 in docs/decisions/2026-05-24-blocked-items.md).
145
+ // serving must also zero the cross-tenant counters for other tenants.
146
146
  export async function handleSleep({ req, res, opts }) {
147
147
  // Defensive per-request loopback guard. Uses the canonical isLoopback()
148
148
  // helper above so any future extension (additional mapped/IPv6 forms,
@@ -151,7 +151,7 @@ export async function handleSleep({ req, res, opts }) {
151
151
  if (!isLoopback(req.socket.remoteAddress)) {
152
152
  throw new HttpError(403, '/v1/sleep is loopback-only (host-wide consolidation; see CHANGELOG v1.11.4)');
153
153
  }
154
- // v1.12.0 A5 v2 sub-1: admin-role gate. Forward-defensive — exists today
154
+ // Admin-role gate. Forward-defensive: exists today
155
155
  // under loopback-only enforcement (loopback fallback is admin by default;
156
156
  // any Bearer-authed caller now carries an explicit role from the api_keys
157
157
  // row). When non-loopback serving lands, this gate is the actual auth
@@ -168,7 +168,7 @@ export async function handleSleep({ req, res, opts }) {
168
168
  if (noShareRaw !== undefined && !isJsonBoolean(noShareRaw)) {
169
169
  throw new HttpError(400, 'no_share must be a boolean');
170
170
  }
171
- // v1.12.0: sleepCtx already built above for the admin-role gate; reuse.
171
+ // sleepCtx already built above for the admin-role gate; reuse.
172
172
  const result = await sleep(sleepCtx, {
173
173
  dryRun: dryRunRaw === true,
174
174
  noShare: noShareRaw === true,
@@ -3,7 +3,8 @@ import { closePolicy, loadPolicies, loadPoliciesAsOf, loadPolicyById, savePolicy
3
3
  import { HttpError, sendJson } from '../../http-util.js';
4
4
  import { buildContextWithAuth } from '../auth.js';
5
5
  import { byCreatedAt, pageOf, parseCursor } from '../cursor.js';
6
- import { isJsonString, isSetMember, parseJsonBody, parseListLimit } from '../validation.js';
6
+ import { isSetMember, parseJsonBody, parseListLimit } from '../validation.js';
7
+ import { isJsonString } from '../../json.js';
7
8
  // HTTP-boundary check for an optional policy date field (validFrom/validTo).
8
9
  // Type + length only; savePolicy/loadPoliciesAsOf normalize + format-validate the
9
10
  // value (an unparseable date throws there -> mapped to 400). 64-char cap bounds a
@@ -19,7 +20,7 @@ function optionalDateField(raw, label) {
19
20
  }
20
21
  return raw;
21
22
  }
22
- // ── policies (E2 first-class object, bi-temporal-first) ──
23
+ // ── policies (first-class object, bi-temporal-first) ──
23
24
  //
24
25
  // 6 routes: POST /v1/policies (new; processName-style body policyName +
25
26
  // policyText + validFrom? + validTo?), GET /v1/policies (list, status filter),
@@ -1,17 +1,17 @@
1
1
  // /v1/predictions routes.
2
- import { closePrediction, computePredictionBaserate, loadOpenPredictions, loadPredictionById, loadPredictionsByClass, savePrediction, VALID_CLOSURE_STATES } from '../../predictions/store.js';
3
- import { HttpError, sendJson } from '../../http-util.js';
2
+ import { closePrediction, computePredictionBaserate, loadAllPredictions, loadOpenPredictions, loadPredictionById, loadPredictionsByClass, savePrediction, VALID_CLOSURE_STATES } from '../../predictions/store.js';
3
+ import { HttpError, MAX_ID_LEN, sendJson } from '../../http-util.js';
4
4
  import { buildContextWithAuth } from '../auth.js';
5
5
  import { byCreatedAt, pageOf, parseCursor } from '../cursor.js';
6
- import { isJsonNumber, isJsonString, isSetMember, parseJsonBody, parseListLimit } from '../validation.js';
7
- // ── E2 prediction first-class object (v0.31) ──
8
- // docs/plans/2026-05-26-e2-prediction-object.md
6
+ import { isJsonNumber, isSetMember, parseJsonBody, parseListLimit } from '../validation.js';
7
+ import { isJsonString } from '../../json.js';
8
+ // ── prediction first-class object ──
9
9
  //
10
10
  // 4 routes: POST /v1/predictions (create), GET /v1/predictions (list),
11
11
  // GET /v1/predictions/:id (show), POST /v1/predictions/:id/close (close).
12
12
  // All Bearer-authed + tenant-scoped via buildContextWithAuth. closure_state
13
13
  // validated against VALID_CLOSURE_STATES (3 states). DoS caps on claim
14
- // (4096 chars) + closureNote (2048 chars) per v1.11.4 pattern.
14
+ // (4096 chars) + closureNote (2048 chars).
15
15
  export async function handleCreatePrediction({ req, res, opts }) {
16
16
  const ctx = await buildContextWithAuth(req, opts);
17
17
  const body = await parseJsonBody(req, ctx);
@@ -68,12 +68,9 @@ export async function handleListPredictions({ req, res, opts, query }) {
68
68
  const ctx = await buildContextWithAuth(req, opts);
69
69
  let predictions;
70
70
  if (status === 'all') {
71
- if (classTag) {
72
- predictions = loadPredictionsByClass(opts.hippoRoot, ctx.tenantId, classTag, { limit: limit + 1, after });
73
- }
74
- else {
75
- predictions = loadOpenPredictions(opts.hippoRoot, ctx.tenantId, { limit: limit + 1, after });
76
- }
71
+ predictions = classTag
72
+ ? loadPredictionsByClass(opts.hippoRoot, ctx.tenantId, classTag, { limit: limit + 1, after })
73
+ : loadAllPredictions(opts.hippoRoot, ctx.tenantId, { limit: limit + 1, after });
77
74
  }
78
75
  else if (status === 'open') {
79
76
  predictions = loadOpenPredictions(opts.hippoRoot, ctx.tenantId, {
@@ -99,7 +96,7 @@ export async function handleListPredictions({ req, res, opts, query }) {
99
96
  sendJson(res, 200, { predictions: page.items, next_cursor: page.nextCursor });
100
97
  return;
101
98
  }
102
- // J3 reference-class / planning-fallacy detector (v0.31).
99
+ // Reference-class / planning-fallacy detector.
103
100
  // Order matters: this must match BEFORE /v1/predictions/:id since 'stats'
104
101
  // is not a number — the :id regex requires \d+ so they don't conflict,
105
102
  // but routing this first avoids the dispatch order risk.
@@ -108,8 +105,8 @@ export async function handlePredictionStats({ req, res, opts, query }) {
108
105
  if (!classTag || classTag.length === 0) {
109
106
  throw new HttpError(400, 'class param is required');
110
107
  }
111
- if (classTag.length > 256) {
112
- throw new HttpError(400, 'class exceeds 256-character cap');
108
+ if (classTag.length > MAX_ID_LEN) {
109
+ throw new HttpError(400, `class exceeds ${MAX_ID_LEN}-character cap`);
113
110
  }
114
111
  const ctx = await buildContextWithAuth(req, opts);
115
112
  const baserate = computePredictionBaserate(opts.hippoRoot, ctx.tenantId, classTag, ctx.actor.subject);